**TCP/IP协议栈在Linux内核中的运行时序分析**

时间:2021-01-29 12:03:56   收藏:0   阅读:0

TCP/IP协议栈在Linux内核中的运行时序分析

调研要求:

1、在深入理解Linux内核任务调度(中断处理、softirg、tasklet、wq、内核线程等)机制的基础上,分析梳理send和recv过程中TCP/IP协议栈相关的运行任务实体及相互协作的时序分析。

2、编译、部署、运行、测评、原理、源代码分析、跟踪调试等

3、应该包括时序图

Linux内核任务调度

中断

中断定义

中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。

当一个中断发生时,操作系统必须确保下面的步骤顺序:

中断类型

笼统地来讲,可以把中断分为两个主要类型:

外部中断,由 Local APIC 或者与 Local APIC 连接的处理器针脚接收。 软件引起的中断,由处理器自身的特殊情况引起(有时使用特殊架构的指令)。一个常见的关于特殊情况的例子就是 除零。另一个例子就是使用 系统调用(syscall) 退出程序。

中断可以在任何时间因为超出代码和 CPU 控制的原因而发生。另一方面,异常和程序执行同步(synchronous) ,并且可以被分为 3 类:故障(Faults)、陷入(Traps)、终止(Aborts)。

中断可以分为 可屏蔽的(maskable) 和 不可屏蔽的(non-maskable)。可屏蔽的中断可以被阻塞,使用 x86_64 的指令 - sticli。可以在 Linux 内核代码中找到:

static inline void native_irq_disable(void)
{
        asm volatile("cli": : :"memory");
}

and

static inline void native_irq_enable(void)
{
        asm volatile("sti": : :"memory");
}

这两个指令修改了在中断寄存器中的 IF 标识位。 sti 指令设置 IF 标识,cli 指令清除这个标识。不可屏蔽的中断总是被报告。通常,任何硬件上的失败都映射为不可屏蔽中断。

延后中断

中断处理会有一些特点,其中最主要的两个是:

但是不可能同时做到这两点,因此之前的中断被分为两个部分:

中断处理代码运行于中断处理上下文中,此时禁止响应后续的中断,所以要避免中断处理代码长时间执行。但有些中断却又需要执行很多工作,所以中断处理有时会被分为两部分。第一部分中,中断处理先只做尽量少的重要工作,接下来提交第二部分给内核调度,然后就结束运行。当系统比较空闲并且处理器上下文允许处理中断时,第二部分被延后的剩余任务就会开始执行。

当前实现延后中断的有如下三种途径:

Softirq

伴随着内核对并行处理的支持,出于性能考虑,所有新的下半部实现方案都基于被称之为 ksoftirqd 的内核线程。每个处理器都有自己的内核线程,名字叫做 ksoftirqd/n,n是处理器的编号。可以通过系统命令 systemd-cgls 看到这些线程。

$ systemd-cgls -k | grep ksoft
├─   3 [ksoftirqd/0]
├─  13 [ksoftirqd/1]
├─  18 [ksoftirqd/2]
├─  23 [ksoftirqd/3]
├─  28 [ksoftirqd/4]
├─  33 [ksoftirqd/5]
├─  38 [ksoftirqd/6]
├─  43 [ksoftirqd/7]

spawn_ksoftirqd 函数启动这些线程。

软中断在 Linux 内核编译时就静态地确定了。open_softirq 函数负责 softirq 初始化,它在 kernel/softirq.c 中定义:

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

这个函数有两个参数:

首先来看 softirq_vec 数组:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

它在同一源文件中定义。softirq_vec 数组包含了 NR_SOFTIRQS (其值为10)个不同 softirq 类型的 softirq_action。当前版本的 Linux 内核定义了十种软中断向量。其中两个 tasklet 相关,两个网络相关,两个块处理相关,两个定时器相关,另外调度器和 RCU 也各占一个。所有这些都在一个枚举中定义:

enum
{
        HI_SOFTIRQ=0,
        TIMER_SOFTIRQ,
        NET_TX_SOFTIRQ,
        NET_RX_SOFTIRQ,
        BLOCK_SOFTIRQ,
        BLOCK_IOPOLL_SOFTIRQ,
        TASKLET_SOFTIRQ,
        SCHED_SOFTIRQ,
        HRTIMER_SOFTIRQ,
        RCU_SOFTIRQ,
        NR_SOFTIRQS
};

open_softirq 函数实际上用 softirq_action 参数填充了 softirq_vec 数组。由 open_softirq 注册的延后中断处理函数会由 raise_softirq 调用。这个函数只有一个参数 — 软中断序号 nr。它的实现如下:

void raise_softirq(unsigned int nr)
{
        unsigned long flags;
        local_irq_save(flags);
        raise_softirq_irqoff(nr);
        local_irq_restore(flags);
}

可以看到在 local_irq_savelocal_irq_restore 两个宏中间调用了 raise_softirq_irqoff 函数。local_irq_save 的定义位于 include/linux/irqflags.h 头文件,它保存了 eflags 寄存器中的 IF 标志位并且禁用了当前处理器的中断。

local_irq_restore 宏定义于相同头文件中,它做了完全相反的事情:装回之前保存的中断标志位然后允许中断。这里之所以要禁用中断是因为将要运行的 softirq 中断处理运行于中断上下文中。

raise_softirq_irqoff 函数设置当前处理器上和nr参数对应的软中断标志位(__softirq_pending)。

每个 softirq 都有如下的阶段:通过 open_softirq 函数注册一个软中断,通过 raise_softirq 函数标记一个软中断来激活它,然后所有被标记的软中断将会在 Linux 内核下一次执行周期性软中断检测时得以调度,对应此类型软中断的处理函数也就得以执行。

因此,可以看出,软中断是静态分配的,这对于后期加载的内核模块将是一个问题。基于软中断实现的 tasklets 解决了这个问题。

Tasklets

内核中实现延后中断的主要途径是 tasklets,它是构建于 softirq 中断之上,基于下面两个软中断实现的:

简而言之,tasklets 是运行时分配和初始化的软中断。和软中断不同的是,同一类型的 tasklets 可以在同一时间运行于不同的处理器上。 softirq_init 函数函数在 kernel/softirq.c 中定义如下:

void __init softirq_init(void)
{
        int cpu;
        for_each_possible_cpu(cpu) {
                per_cpu(tasklet_vec, cpu).tail =
                        &per_cpu(tasklet_vec, cpu).head;
                per_cpu(tasklet_hi_vec, cpu).tail =
                        &per_cpu(tasklet_hi_vec, cpu).head;
        
        open_softirq(TASKLET_SOFTIRQ, tasklet_action);
        open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

上面代码中,在 softirq_init 函数中初始化了两个 tasklets 数组:tasklet_vectasklet_hi_vec。Tasklets 和高优先级 Tasklets 分别存储于这两个数组中。初始化完成后我们看到代码 kernel/softirq.csoftirq_init 函数的最后又两次调用了 open_softirq

open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);

open_softirq 函数的主要作用是初始化软中断。和 Tasklets 相关的软中断处理函数有两个,分别是 tasklet_actiontasklet_hi_action。其中 tasklet_hi_actionHI_SOFTIRQ 关联在一起,tasklet_actionTASKLET_SOFTIRQ 关联在一起。

Linux 内核提供一些 API 供操作 Tasklets 之用。例如, tasklet_init 函数,它接受一个 task_struct 数据结构,一个处理函数,和另外一个参数,并利用这些参数来初始化所给的 task_struct 结构:

void tasklet_init(struct tasklet_struct *t,
                  void (*func)(unsigned long), unsigned long data)
{
    t->next = NULL;
    t->state = 0;
    atomic_set(&t->count, 0);
    t->func = func;
    t->data = data;
}

其中,tasklet_struct 数据类型在 include/linux/interrupt.h 中定义,它代表一个 Tasklet。具体定义:

struct tasklet_struct
{
        struct tasklet_struct *next;
        unsigned long state;
        atomic_t count;
        void (*func)(unsigned long);
        unsigned long data;
};

此数据结构包含有下面5个成员:

tasklet是在软中断之上实现,在实现上做了一些优化,它与软中断的区别:

Workqueues

工作队列是另外一个处理延后函数的概念,它大体上和 tasklets 类似。工作队列运行于内核进程上下文,而 tasklets 运行于软中断上下文。这意味着工作队列函数不必像 tasklets 一样必须是原子性的。Tasklets 总是运行于它提交自的那个处理器,工作队列在默认情况下也是这样。工作队列在 Linux 内核代码 kernel/workqueue.c 中由如下的数据结构表示:

struct worker_pool {
    spinlock_t              lock;
    int                     cpu;
    int                     node;
    int                     id;
    unsigned int            flags;
    struct list_head        worklist;
    int                     nr_workers;
...
...
...

工作队列最基础的用法,是作为创建内核线程的接口来处理提交到队列里的工作任务。所有这些内核线程称之为 worker thread。工作队列内的任务是由代码 include/linux/workqueue.h 中定义的 work_struct 表示的,其定义如下:

 struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

所有的 works 都会在内核线程中执行。当内核线程得到调度,它开始执行 workqueue 中的 works。每一个工作队列内核线程都会在 worker_thread 函数里执行一个循环。

内核线程

Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的。内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。这与用户线程是不一样的。因为内核线程只运行在内核态因此,它只能使用大于PAGE_OFFSET(传统的x86_32上是3G)的地址空间。

内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,它与内核中的其他进程”并行”执行。内核线程经常被称之为内核守护进程。

他们执行下列任务:

内核线程主要有两种类型:

内核线程由内核自身生成,其特点在于:

TCP/IP协议

TCP/IP模型简介

TCP/IP 协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连入因特网,以及数据如何在它们之间进行传输。TCP/IP 协议采用4层结构,分别是应用层、传输层、网络层和链路层,每一层都呼叫它的下一层所提供的协议来完成自己的需求。

下面我们通过一张图先来大概了解一下TCP/IP协议的基本框架:

技术图片

当通过http发起一个请求时,应用层、传输层、网络层和链路层的相关协议依次对该请求进行包装并携带对应的首部,最终在链路层生成以太网数据包,以太网数据包通过物理介质传输给对方主机,对方接收到数据包以后,然后再一层一层采用对应的协议进行拆包,最后把应用层数据交给应用程序处理。

有了整体概念以后,下面了解一下各层的分工:

当你输入一个网址并按下回车键的时候,首先,应用层协议对该请求包做了格式定义;紧接着传输层协议加上了双方的端口号,确认了双方通信的应用程序;然后网络协议加上了双方的IP地址,确认了双方的网络位置;最后链路层协议加上了双方的MAC地址,确认了双方的物理位置,同时将数据进行分组,形成数据帧,采用广播方式,通过传输介质发送给对方主机。而对于不同网段,该数据包首先会转发给网关路由器,经过多次转发后,最终被发送到目标主机。目标机接收到数据包后,采用对应的协议,对帧数据进行组装,然后再通过一层一层的协议进行解析,最终被应用层的协议解析并交给服务器处理。

Socket

Socket简介

Socket接口是TCP/IP网络的API,定义了许多函数或例程,可以用它们来开发TCP/IP网络上的应用程序。Socket独立于具体协议的网络编程接口,在OSI模型中,主要位于会话层和传输层之间。Linux Socket 是从 BSD Socket 发展而来的,它是 Linux 操作系统的重要组成部分之一,它是网络应用程序的基础。

BSD Socket(伯克利套接字)是通过标准的UNIX文件描述符和其它程序通讯的一个方法,目前已经被广泛移植到各个平台。

TCP网络程序调用Socket APi的次序,如图所示:

技术图片

Socket接口函数

socket是“open—write/read—close”模式的一种实现,提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。

socket()函数

int socket(int domain, int type, int protocol);
//功能:创建一个新的套接字,返回套接字描述符
//参数说明:
/*
  domain:域类型,指明使用的协议栈,如TCP/IP使用的是 PF_INET	
  type: 指明需要的服务类型, 如
  SOCK_DGRAM: 数据报服务,UDP协议
  SOCK_STREAM: 流服务,TCP协议
  protocol:一般都取0
举例:s=socket(PF_INET,SOCK_STREAM,0)
*/

bind()函数

int bind(int sockfd,struct sockaddr * my_addr,int addrlen)
/*
  功能:为套接字指明一个本地端点地址TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号,服务器使用它来指  明熟知的端口号,然后等待连接
  参数说明:
  Sockfd:套接字描述符,指明创建连接的套接字
  my_addr:本地地址,IP地址和端口号
  addrlen :地址长度
  举例:bind(sockfd, (struct sockaddr *)&address, sizeof(address)); 
*/

listen()函数

int listen(int sockfd,int input_queue_size)
/*
  功能:面向连接的服务器使用它将一个套接字置为被动模式,并准备接收传入连接。用于服务器,指明某个套接字连接是被动的
  参数说明:
  Sockfd:套接字描述符,指明创建连接的套接字
  input_queue_size:该套接字使用的队列长度,指定在请求队列中允许的最大请求数 
  举例:listen(sockfd,20)
*/

accept()函数

int accept(int sockfd, struct sockaddr *addr, int *addrlen); 
/*
  功能:获取传入连接请求,返回新的连接的套接字描述符。为每个新的连接请求创建了一个新的套接字,服务器只对新的连接使用该套接字,原来的监听套接字接受其他的连接请求。新的连接上传输数据使用新的套接字,使用完毕,服务器将关闭这个套接字。
  参数说明:
  Sockfd:套接字描述符,指明正在监听的套接字
  addr:提出连接请求的主机地址
  addrlen:地址长度
  举例:new_sockfd = accept(sockfd, (struct sockaddr *)&address, &addrlen);
 */

connect()函数

int connect(int sockfd,struct sockaddr *server_addr,int sockaddr_len)
/*
  功能: 同远程服务器建立主动连接,成功时返回0,若连接失败返回-1。
参数说明:
  Sockfd:套接字描述符,指明创建连接的套接字
  Server_addr:指明远程端点:IP地址和端口号
  sockaddr_len :地址长度
 */

sendto()函数

int sendto(int sockfd, const void * data, int data_len, unsigned int flags, struct sockaddr *remaddr,int remaddr_len)
/*
  功能:基于UDP发送数据报,返回实际发送的数据长度,出错时返回-1
  参数说明:
  sockfd:套接字描述符
  data:指向要发送数据的指针
  data_len:数据长度
  flags:一直为0
  remaddr:远端地址:IP地址和端口号
  remaddr_len :地址长度
  举例:sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&address, sizeof(address)); 
 */

send()函数

int send(int sockfd, const void * data, int data_len, unsigned int flags)
/*
  功能:
  在TCP连接上发送数据,返回成功传送数据的长度,出错时返回-1。send会将外发数据复制到OS内核中
  参数说明:
  sockfd:套接字描述符
  data:指向要发送数据的指针
  data_len:数据长度
  flags:一直为0
  举例(p50):send(s,req,strlen(req),0);
*/

recvfrom()函数

int recvfrom(int sockfd, void *buf, int buf_len,unsigned int flags,struct sockaddr *from,int *fromlen);
/*
  功能:从UDP接收数据,返回实际接收的字节数,失败时返回-1
  参数说明:
  Sockfd:套接字描述符
  buf:指向内存块的指针
  buf_len:内存块大小,以字节为单位
  flags:一般为0
  from:远端的地址,IP地址和端口号
  fromlen:远端地址长度
  举例:recvfrom(sockfd,buf,8192,0, ,(struct sockaddr *)&address, &fromlen);
  */

recv()函数

int recv(int sockfd, void *buf, int buf_len,unsigned int flags); 
/*
  功能:
  从TCP接收数据,返回实际接收的数据长度,出错时返回-1。服务器使用其接收客户请求,客户使用它接受服务器的应答。如果没有数据,将阻塞,如果收到的数据大于缓存的大小,多余的数据将丢弃。
  参数说明:
  Sockfd:套接字描述符
  Buf:指向内存块的指针
  Buf_len:内存块大小,以字节为单位
  flags:一般为0 
  举例:recv(sockfd,buf,8192,0)
  */

close()函数

close(int sockfd); 
/*
  功能:
  撤销套接字。如果只有一个进程使用,立即终止连接并撤销该套接字,如果多个进程共享该套接字,将引用数减一,如果引用数降到零,则撤销它。
  参数说明:
  Sockfd:套接字描述符
  举例:close(socket_descriptor)
  */

send和recv过程时序分析

应用层分析

发送端

send的定义如下所示:

ssize_t send(int sockfd, const void *buf, size_t len, int flags)

应用层发送数据时,调用send()函数时,内核封装send()sendto(),然后发起系统调用。其实也很好理解,send()就是sendto()的一种特殊情况,而sendto()在内核的系统调用服务程序为sys_sendto

int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags,
		 struct sockaddr __user *addr,  int addr_len)
{
	struct socket *sock;
	struct sockaddr_storage address;
	int err;
	struct msghdr msg;
	struct iovec iov;
	int fput_needed;
	err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
	if (unlikely(err))
		return err;
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (!sock)
		goto out;

	msg.msg_name = NULL;
	msg.msg_control = NULL;
	msg.msg_controllen = 0;
	msg.msg_namelen = 0;
	if (addr) {
		err = move_addr_to_kernel(addr, addr_len, &address);
		if (err < 0)
			goto out_put;
		msg.msg_name = (struct sockaddr *)&address;
		msg.msg_namelen = addr_len;
	}
	if (sock->file->f_flags & O_NONBLOCK)
		flags |= MSG_DONTWAIT;
	msg.msg_flags = flags;
	err = sock_sendmsg(sock, &msg);

out_put:
	fput_light(sock->file, fput_needed);
out:
	return err;
}

__sys_sendto函数其实做了3件事:

在返回时调用的是sock_sendmsg函数,继续追踪这个函数。

int sock_sendmsg(struct socket *sock, struct msghdr *msg)

{

    int err = security_socket_sendmsg(sock, msg,
    
                      msg_data_left(msg));
    
    return err ?: sock_sendmsg_nosec(sock, msg);

}

EXPORT_SYMBOL(sock_sendmsg);

static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)

{

    int ret = INDIRECT_CALL_INET(sock->ops->sendmsg, inet6_sendmsg,
    
                     inet_sendmsg, sock, msg,
    
                     msg_data_left(msg));
    
    BUG_ON(ret == -EIOCBQUEUED);
    
    return ret;

}

会看到最终调用的是sock->ops->sendmsg(sock, msg, msg_data_left(msg));,tcp协议sendmsg被初始化为tcp_sendmsgtcp_sendmsg 具体负责传输层协议的操作细节,并传到网络层处理函数。所以到tcp_sendmsg为止,应用层的各个函数调用和时序过程追踪完毕,结果如下图:

技术图片

接收端

对于recv函数,与send类似,自然也是recvfrom的特殊情况,调用的也就是__sys_recvfrom,整个函数的调用路径与send非常类似。__sys_recvfrom函数如下:

int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
		   struct sockaddr __user *addr, int __user *addr_len)
{
	......
	err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
	if (unlikely(err))
		return err;
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	.....
	msg.msg_control = NULL;
	msg.msg_controllen = 0;
	/* Save some cycles and don‘t copy the address if not needed */
	msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
	/* We assume all kernel code knows the size of sockaddr_storage */
	msg.msg_namelen = 0;
	msg.msg_iocb = NULL;
	msg.msg_flags = 0;
	if (sock->file->f_flags & O_NONBLOCK)
		flags |= MSG_DONTWAIT;
	err = sock_recvmsg(sock, &msg, flags);

	if (err >= 0 && addr != NULL) {
		err2 = move_addr_to_user(&address,
					 msg.msg_namelen, addr, addr_len);
	.....

}

__sys_recvfrom调用了sock_recvmsg来接收数据,继续追踪这个函数,。

int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags)

{

    int err = security_socket_recvmsg(sock, msg, msg_data_left(msg), flags);
    
    return err ?: sock_recvmsg_nosec(sock, msg, flags);

}

EXPORT_SYMBOL(sock_recvmsg);

static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg,

                     int flags)

{

    return INDIRECT_CALL_INET(sock->ops->recvmsg, inet6_recvmsg,
    
                  inet_recvmsg, sock, msg, msg_data_left(msg),
    
                  flags);

}

整个函数实际调用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);tcp协议对应的recvmsg被初始化为tcp_recvmsg,和send的过程基本上完全类似,断点追踪如下:

技术图片

传输层

发送端

TCP协议对发送数据相关系统调用内核实现,虽然发送相关的系统调用接口由很多,但是到了TCP协议层,都统一由tcp_sendmsg()处理。tcp_sendmsg()函数要完成的工作就是将应用程序要发送的数据组织成skb,然后调用tcp_push函数。查看该tcp_sendmsg()函数,代码如下:

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)

{

    int ret;
    
    lock_sock(sk);
    
    ret = tcp_sendmsg_locked(sk, msg, size);
    
    release_sock(sk);
    
    return ret;

}

EXPORT_SYMBOL(tcp_sendmsg);

从这段代码可以看出,发送的过程涉及到上锁和释放锁的一个操作,目的是让接收和发送队列能够有序进行相关的工作。然后主要的发送函数即为tcp_sendmsg_locked这个函数,继续追踪该函数。

1 int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
 2 
 3 {
 4 
 5     struct tcp_sock *tp = tcp_sk(sk);
 6 
 7     struct ubuf_info *uarg = NULL;40 
41         if (!zc)
42 
43             uarg->zerocopy = 0;
44 
45     }
46 
47    ........
48 
49     if (((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) &&
50 
51         !tcp_passive_fastopen(sk)) {
52 
53         err = sk_stream_wait_connect(sk, &timeo);
54 
55         if (err != 0)
56 
57             goto do_error;
58 
59     }........
60 
61 wait_for_sndbuf:
62 
63         set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
64 
65 wait_for_memory:
66 
67         if (copied)
68 
69             tcp_push(sk, flags & ~MSG_MORE, mss_now,
70 
71                  TCP_NAGLE_PUSH, size_goal);...........
72 
73 }
74 
75 EXPORT_SYMBOL_GPL(tcp_sendmsg_locked);

tcp_sendmsg_locked中,完成的是将所有的数据组织成发送队列,这个发送队列是struct sock结构中的一个域sk_write_queue,这个队列的每一个元素是一个skb,里面存放的就是待发送的数据。在该函数中通过调用tcp_push()函数将数据加入到发送队列中。查看该tcp_push()函数,代码如下:

static void tcp_push(struct sock *sk, int flags, int mss_now, int nonagle, int size_goal)

{

    struct tcp_sock *tp = tcp_sk(sk);
    
    struct sk_buff *skb;
    
    skb = tcp_write_queue_tail(sk);
    
    if (flags & MSG_MORE)
    
        nonagle = TCP_NAGLE_CORK;......
    
    __tcp_push_pending_frames(sk, mss_now, nonagle);

}

tcp_write_xmit()该函数是TCP发送新数据的核心函数,包括发送窗口判断、拥塞控制判断等核心操作都是在该函数中完成。它又将调用发送函数tcp_transmit_skb函数。

1 static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask)
 4 
 5 {
 6 
 7     return __tcp_transmit_skb(sk, skb, clone_it, gfp_mask,
 8 
 9                   tcp_sk(sk)->rcv_nxt);
10 
11 }

在__tcp_transmit_skb这个函数中,我们看到了如下代码:

/* Leave earliest departure time in skb->tstamp (skb->skb_mstamp_ns) */

/* Cleanup our debris for IP stacks */

memset(skb->cb, 0, max(sizeof(struct inet_skb_parm),

               sizeof(struct inet6_skb_parm)));

tcp_add_tx_delay(skb, tp);

err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);

if (unlikely(err > 0)) {

   tcp_enter_cwr(sk);

   err = net_xmit_eval(err);

}

if (!err && oskb) {

    tcp_update_skb_after_send(sk, oskb, prior_wstamp);

    tcp_rate_skb_sent(sk, oskb);

}

return err;

gdb调试结果如下:

技术图片

接收端

tcp_v4_rcv函数为TCP的总入口,数据包从IP层传递上来,进入该函数。tcp_v4_rcv函数只要做以下几个工作:

技术图片

网路层

发送端

1)网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。网络层将数据链路层提供的帧组成数据包,包中封装有网络层包头,其中含有逻辑地址信息- -源站点和目的站点地址的网络地址。其主要任务包括

2)IP 栈基本处理过程如下:ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由。填充IP包的各个字段,比如版本、包头长度、TOS等。中间的一些分片等,可参阅相关文档。基本思想是,当报文的长度大于mtu,gso的长度不为0就会调用 ip_fragment 进行分片,否则就会调用ip_finish_output2把数据发送出去。ip_fragment 函数中,会检查 IP_DF 标志位,如果待分片IP数据包禁止分片,则调用 icmp_send()向发送方发送一个原因为需要分片而设置了不分片标志的目的不可达ICMP报文,并丢弃报文,即设置IP状态为分片失败,释放skb,返回消息过长错误码。用 ip_finish_ouput2 设置链路层报文头了。如果,链路层报头缓存有(即hh不为空),那就拷贝到skb里。如果没,那么就调用neigh_resolve_output,使用 ARP 获取。

3)路由查询从fib_lookup函数开始,之后调用fib_table_lookup函数,函数中加锁进行同步控制,互斥访问fib_table路由表数据结构,得到的路由查询结果以fib_result数据结构返回。在fib_table_lookup中,我们可以发现,路由表中的网络地址是被字典树tire统一组织的,这使得查找最长匹配路径的效率很高。

结合我们上面在传输层追踪到的最后的函数ip_queue_xmit,可以查看到其中的源码:

1 static inline int ip_queue_xmit(struct sock *sk, struct sk_buff *skb,struct flowi *fl)
4 
5 {
6 
7     return __ip_queue_xmit(sk, skb, fl, inet_sk(sk)->tos);
8 
9 } 

ip_queue_xmit调用函数_ip_queue_xmit()。在ip_queue中,会调用skb_rtable函数来检查skb是否已被路由,即获取其缓存信息,将缓存信息保存在变量rt中,如果rt不为空,就直接进行packet_routed函数,如果rt不为空,就会自行ip_route_output_ports查找路由缓存。_ip_queue_xmit()函数,代码如下:

 1 int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl, __u8 tos)
 4 
 5 {................
 6 
 7     int res;.........
 8 
 9         rt = ip_route_output_ports(net, fl4, sk,24 
25             goto no_route;
26 
27         sk_setup_caps(sk, &rt->dst);
28 
29     }
30 
31     skb_dst_set_noref(skb, &rt->dst);
32 
33 packet_routed:
34 
35     if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway)
36 
37         goto no_route;
38 
39     /* OK, we know where to send it, allocate and build IP header. */
40 
41     skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
42 
43    ..............
44 
45     iph->protocol = sk->sk_protocol;
46 
63 no_route:
64 
65     rcu_read_unlock();
66 
67     IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
68 
69     kfree_skb(skb);
70 
71     return -EHOSTUNREACH;
72 
73 }
74 
75 EXPORT_SYMBOL(__ip_queue_xmit); 

ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由。紧接着根据代码可知,会进行分片和字段填充等工作,根据我们所学知识可知,如果大于最大长度mtu,则进行分片,否则直接发出去,调用的函数是ip_finish_output,进而调用__ip_finish_output

 1 static int __ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
 2 
 3 {
 4 
 5     unsigned int mtu;
 6 
 7 #if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM)
 8 
 9     /* Policy lookup after SNAT yielded a new policy */
10 
11     if (skb_dst(skb)->xfrm) {
12 
13         IPCB(skb)->flags |= IPSKB_REROUTED;
14 
15         return dst_output(net, sk, skb);
16 
17     }
18 
19 #endif
20 
21     mtu = ip_skb_dst_mtu(sk, skb);
22 
23     if (skb_is_gso(skb))
24 
25         return ip_finish_output_gso(net, sk, skb, mtu);
26 
27     if (skb->len > mtu || (IPCB(skb)->flags & IPSKB_FRAG_PMTU))
28 
29         return ip_fragment(net, sk, skb, mtu, ip_finish_output2);
30 
31     return ip_finish_output2(net, sk, skb);
32 
33 }

ip_finish_output调用了 ip_finish_output2()ip_finish_output2函数会检测skb的前部空间是否还能存储链路层首部。如果不够,就会申请更大的存储空间,最终会调用邻居子系统的输出函数neigh_output进行输出。输出分为有二层头缓存和没有两种情况,有缓存时调用neigh_hh_output进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出。不管执行哪个函数,最终都会调用dev_queue_xmit将数据包传入数据链路层。

 1 static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
 3 {
 5     struct dst_entry *dst = skb_dst(skb);12 
13     struct neighbour *neigh;
14 
15     bool is_v6gw = false;
16 
17 ..............
18 
19     if (!IS_ERR(neigh)) {
20 
21         int res;
22 
23         sock_confirm_neigh(skb, neigh);
24 
25         /* if crossing protocols, can not use the cached header */
26 
27         res = neigh_output(neigh, skb, is_v6gw);
28 
29         rcu_read_unlock_bh();
30 
31         return res;
32 
33     }
34 
35 static inline int neigh_output(struct neighbour *n, struct sk_buff *skb, bool skip_cache)
38 
39 {
40 
41     const struct hh_cache *hh = &n->hh;
42 
43     if ((n->nud_state & NUD_CONNECTED) && hh->hh_len && !skip_cache)
44 
45         return neigh_hh_output(hh, skb);
46 
47     else
48 
49         return n->output(n, skb);
50 
51 }
52 
53 static inline int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb)
55 {
57     ................
58 
59     __skb_push(skb, hh_len);
60 
61     return dev_queue_xmit(skb);
62 
63 }

断点追踪调试的协议栈如下图所示:

技术图片

接收端

网络IP层的入口函数在ip_rcv函数。ip_rcv函数调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。ip_rcv数首先会检查检验和等各种字段,如果数据包的长度超过最大传送单元MTU的话,会进行分片,最终到达 ip_rcv_finish 函数。查看源码可以看到:

 1 int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,struct net_device *orig_dev)
 4 
 5 {   struct net *net = dev_net(dev);
 6 
 7     skb = ip_rcv_core(skb, net);
 8 
 9     if (skb == NULL)
10 
11         return NET_RX_DROP;
12 
13     return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
14 
15                net, NULL, skb, dev, NULL,
16 
17                ip_rcv_finish);
18 
19 }

最终调用的是ip_rcv_finish这个函数接口,ip_rcv_finish 函数会调用dst_input函数,当缓存查找没有匹配路由时将调用ip_route_input_slow(),决定该 package 将会被发到本机还是会被转发还是丢弃。

static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)

{

    struct net_device *dev = skb->dev;

    int ret;

    /* if ingress device is enslaved to an L3 master device pass the

     * skb to its handler for processing

     */

    skb = l3mdev_ip_rcv(skb);

    if (!skb)

        return NET_RX_SUCCESS;

    ret = ip_rcv_finish_core(net, sk, skb, dev);

    if (ret != NET_RX_DROP)

        ret = dst_input(skb);

    return ret;

}

static inline int dst_input(struct sk_buff *skb)

{

    return skb_dst(skb)->input(skb);

}

? 在dst_input函数中,最终在ip层即生成ip_input,根据路由选择调用ip_router_input 函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃。如果是发到本机的将会执行ip_local_deliver函数。可能会做 de-fragment(合并多个包),并调用ip_local_deliver_finish。ip_local_deliver_finish会调用ip_protocol_deliver_rcu函数。

根据源码可以看出发向上层的数据时调用 ip_local_deliver 函数,可能会合并IP包,然后调用 ip_local_deliver 函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv等,对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而处理流程进入 TCP 栈。由此可以和我们刚刚追踪的传输层的函数连接起来;当然,跟新路由的时候如果是转发而不是发送到本机则向下层处理。

 1 int ip_local_deliver(struct sk_buff *skb) {
11     struct net *net = dev_net(skb->dev);
12 
13     if (ip_is_fragment(ip_hdr(skb))) {
14 
15         if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
16 
17             return 0;
18 
19     }
20 
21     return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
22 
23                net, NULL, skb, skb->dev, NULL,
24 
25                ip_local_deliver_finish);
26 
27 }
28 
29 static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
30 
31 {
32 
33     __skb_pull(skb, skb_network_header_len(skb));
34 
35     rcu_read_lock();
36 
37     ip_protocol_deliver_rcu(net, skb, ip_hdr(skb)->protocol);
38 
39     rcu_read_unlock();
40 
41     return 0;
42 
43 }

上面的第二个函数中,最后调用的 ip_protocol_deliver_rcu即为具体的选择协议的函数,将输入数据包从网络层传递到传输层。

 1 void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol)
 2 
 3 {
 4 
 5     const struct net_protocol *ipprot;
 6 
 7     int raw, ret;
 8 
 9 resubmit:
10 
11     raw = raw_local_deliver(skb, protocol);
12 
13     ipprot = rcu_dereference(inet_protos[protocol]);
14 
15     if (ipprot) {
16 
17         if (!ipprot->no_policy) {
18 
19             if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
20 
21                 kfree_skb(skb);
22 
23                 return;
24 
25             }
26 
27             nf_reset_ct(skb);
29         }
30 
31         ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv,skb);
37 }

此处,根据tcp协议,调用了tcp_v4_rcv函数,向上进入传输层处理。即网络层的接收追踪完毕,断点调试如下所示:

技术图片

数据链路层和物理层

发送端

数据链路层的入口是dev_queue_xmit函数。即在这个函数入口这里进入链路层进行处理。

1 int dev_queue_xmit(struct sk_buff *skb)
2 
3 {
4 
5     return __dev_queue_xmit(skb, NULL);
6 
7 } 

dev_queue_xmit函数对不同类型的数据包进行不同的处理。__dev_queue_xmit会调用dev_hard_start_xmit函数获取skb

__dev_queue_xmit的部分代码如下:

 1 static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
 2 
 3 { .........
 4 
 5     bool again = false;
 6 
 7     if (dev->flags & IFF_UP) {
 8 
 9         int cpu = smp_processor_id(); /* ok because BHs are off */
29                 skb = dev_hard_start_xmit(skb, dev, txq, &rc);
30 
31                 dev_xmit_recursion_dec();
32 
33                 if (dev_xmit_complete(rc)) {
34 
35                     HARD_TX_UNLOCK(dev, txq);
36 
37                     goto out;

涉及到链路层的各个方面的检查,流量控制,封装成帧等,不过多去看他的代码实现,这里只关注一切正常的情况下,调用dev_hard_start_xmit函数向下发送数据。dev_hard_start_xmit函数会循环调用xmit_one函数,直到将待输出的数据包提交给网络设备的输出接口,完成数据包的输出。

 1 struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,
 2 
 3            ......
 4 
 5     while (skb) {
 6 
 7         struct sk_buff *next = skb->next;
 8 
 9         skb_mark_not_on_list(skb);
10 
11         rc = xmit_one(skb, dev, txq, next != NULL);
12 
13         .....

xmit_one中调用__net_dev_start_xmit函数。一旦网卡完成报文发送,将产生中断通知 CPU,然后驱动层中的中断处理程序就可以删除保存的 skb。最终的数据通过xmit_one这个函数传递给物理层的设备,到这里虚拟的传递的驱动就要结束了,将和实际的设备驱动连接起来.

 1 static int xmit_one(struct sk_buff *skb, struct net_device *dev,struct netdev_queue *txq, bool more)
 5 {
 6 
 7     unsigned int len;
 8 
 9     int rc;
10 
11     if (dev_nit_active(dev))
12 
13         dev_queue_xmit_nit(skb, dev);
14 
15     len = skb->len;
16 
17     trace_net_dev_start_xmit(skb, dev);
18 
19     rc = netdev_start_xmit(skb, dev, txq, more);
20 
21     trace_net_dev_xmit(skb, rc, dev, len);
23     return rc;
25 } 

gdb调试如下图:

技术图片

接收端

接收端的过程较为简单,因为从物理设备上到链路层,还是很底层的东西,所以会经过很多中断处理的过程,其中就包括我们前面所说的软中断。

? 当一个包到达物理层网络时,接收到数据帧就会引发中断。接收端中断处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。接受数据的入口函数是net_rx_action,在net_rx_action函数中会去调用设备的napi_poll函数, 它是设备自己注册的。

 1 static __latent_entropy void net_rx_action(struct softirq_action *h)
 2 
 3 {
 4 
 5     struct softnet_data *sd = this_cpu_ptr(&softnet_data);
 6 
 7     unsigned long time_limit = jiffies +
 8 
 9         local_irq_disable();
10 
11     list_splice_init(&sd->poll_list, &list);
12 
13     local_irq_enable();
14 
15     for (;;) {
16 
17         struct napi_struct *n;
18 
19         budget -= napi_poll(n, &repoll);
20 
21         /* If softirq window is exhausted then punt.
22 
23          * Allow this to run for 2 jiffies since which will allow
24 
25          * an average latency of 1.5/HZ.
43 out:
44 
45     __kfree_skb_flush();
46 
47 }

在设备的napi_poll函数中, 它负责调用napi_gro_receive函数。

static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{
    void *have;
    int work, weight;
    list_del_init(&n->poll_list);
    have = netpoll_poll_lock(n);
    weight = n->weight;
    work = 0;
    if (test_bit(NAPI_STATE_SCHED, &n->state)) {
        work = n->poll(n, weight);  //调用网卡注册的poll函数
        trace_napi_poll(n, work, weight);
    } 
    WARN_ON_ONCE(work > weight);
    if (likely(work < weight))
        goto out_unlock;
    if (unlikely(napi_disable_pending(n))) {
        napi_complete(n);
        goto out_unlock;
    }
    if (n->gro_list) {
        napi_gro_flush(n, HZ >= 1000);
    }
    if (unlikely(!list_empty(&n->poll_list))) {
        pr_warn_once("%s: Budget exhausted after napi rescheduled\n",
                 n->dev ? n->dev->name : "backlog");
        goto out_unlock;
    }
    list_add_tail(&n->poll_list, repoll); 
out_unlock:
    netpoll_poll_unlock(have);
 
    return work;
}

napi_gro_receive用来将网卡上的数据包发给协议栈处理。它会调用 netif_receive_skb_core。而它会调用__netif_receive_skb_one_core,将数据包交给上层 ip_rcv 进行处理。

gdb调试结果如下图:

技术图片

时序图

技术图片

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!