TCP/IP协议栈在Linux内核中的运行时序分析
0、调研要求
- 在深入理解Linux内核任务调度(中断处理、softirg、tasklet、wq、内核线程等)机制的基础上,分析梳理send和recv过程中TCP/IP协议栈相关的运行任务实体及相互协作的时序分析。
- 编译、部署、运行、测评、原理、源代码分析、跟踪调试等
- 时序图
目录
1 Linux内核基础简介
1.1 Linux中断处理
1.2 softirq、tasklet、work queue
2 TCP/IP协议栈
2.1 TCP/IP协议栈简介
2.2 Linux网络分层结构
2.3 socket
3 跟踪调试内核函数的环境
4 send过程的Linux内核实现
4.1 应用层
4.2 传输层
4.3 网络层
4.4 数据链路层和物理层
5 recv过程的Linux内核实现
5.1 应用层
5.2 传输层
5.3 网络层
5.4 数据链路层和物理层
6 recv、sendt通信过程的时序图
7 参考文献
1 Linux内核基础简介
1.1 Linux中断处理
- 中断定义:中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。
- 作用:提供特殊的机制,使得CPU转去运行正常程序之外的代码,从而避免让高速的CPU迁就低俗的外设。
- 中断分类:分为异步的中断和同步的异常,中断又分为可屏蔽中断和不可屏蔽中断,异常又分为处理器探测异常和编程异常。处理器探测异常又进一步分为故障、陷阱、终止。
- 中断向量:0~255之间的一个数,当发生中断时,对应中断向量的电信号被CPU感知,CPU通过idtr寄存器找到中断描述符表的首地址(IDT)。CPU通过中断向量号和IDT首地址找到IDT表中的数据结构对应的段描述符表对应的项,再找到中断处理程序的首地址,并进行权限控制。
- 中断处理过程:
- 确定与中断或者异常关联的中断向量i(0~255)
- 读idtr寄存器指向的IDT表中的第i项
- 从gdtr寄存器获得GDT的基地址,并在GDT中查找,读取IDT表项中的段选择符所标识的段描述符。
- 确定中断是由授权的发生源发处的。
- 检查是否发生了特权级的变化,比如从用户态陷入内核态。
- 如果发生的是故障,修改cs和eip寄存器的值,是发生故障的指令在中断处理结束后再次执行。
- 在内核栈中保存eflags、cs和eip寄存器的内容。
- 如果异常产生硬件出错码,那么也保存到内核栈中。
- 装载cs和eip寄存器,即让CPU转到中断处理程序进行执行。
- 中断处理程序运行。
- 中断处理程序运行结束后,用保存在内核栈的cs、eip、eflags寄存器恢复现场。
- 如果发生特权级变化,那么从内核栈切换回用户栈,即需要装载ss和eip寄存器。
1.2 softirq、tasklet、work queue
- 可延迟中断。Linux把中断过程中的要执行的任务分为紧急、 非紧急、非紧急可延迟三类。在中断处理程序中只完成不可延迟的部分以达到快速响应的目的,并把可延迟的操作内容推迟到内核的下半部分执行,在一个更合适的时机调用函数完成这些可延迟的操作。Linux内核提供了软中断softirq、tasklet和工作队列work queue等方法进行实现。
- 软中断和tasklet包含四种操作:初始化、激活、屏蔽、执行。软中断在Linux的softirq_vec中定义,使用open_softirq进行初始化,触发时调用raise_softirq函数。内核会每隔一段时间到达检查点,查看是否有软中断需要执行,如果有则调用do_softirq进行处理。
- tasklet是IO驱动程序实现可延迟函数的首选,建立在HI_SOFTIRQ和TASKLET_SOFTURQ等软中断之上,tasklet和高优先级的tasklet分别存放在tasklet_vec和tasklet_hi_vec数组中,分别由tasklet_action和tasklet_hi_action处理。
- work queue工作队列。它把工作推后,交给一个内核线程执行,工作队列允许被重新调度甚至睡眠。在Linux中的数据结构是workqueue_struct和cpu_workqueue_struct。
2 TCP/IP协议栈
2.1 TCP/IP协议栈简介
1 TCP/IP协议栈定义。TCP/IP 协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连入因特网,以及数据如何在它们之间进行传输。TCP/IP 协议采用4层结构,分别是应用层、传输层、网络层和链路层,每一层都呼叫它的下 一层所提供的协议来完成自己的需求。
2 各层职责
- 链路层:对0和1进行分组,定义数据帧,确认主机的物理地址,传输数据;
- 网络层:定义IP地址,确认主机所在的网络位置,并通过IP进行MAC寻址,对外网数据包进行路由转发;
- 传输层:定义端口,确认主机上应用程序的身份,并将数据包交给对应的应用程序;
- 应用层:定义数据格式,并按照对应的格式解读数据。
2.2. linux内核ipv4网络部分分层结构
BSD socket层: 这一部分处理BSD socket相关操作,每个socket在内核中以struct socket结构体现。这一部分的文件主要有:/net/socket.c /net/protocols.c etc
INET socket层:BSD socket是个可以用于各种网络协议的接口,而当用于tcp/ip,即建立了AF_INET形式的socket时,还需要保留些额外的参数,于是就有了struct sock结构。文件主要有:/net/ipv4/protocol.c /net/ipv4/af_inet.c /net/core/sock.c
TCP/UDP层:处理传输层的操作,传输层用struct inet_protocol和struct proto两个结构表示。文件主要有:/net/ipv4/udp.c /net/ipv4/datagram.c /net/ipv4/tcp.c
/net/ipv4/tcp_input.c /net/ipv4//tcp_output.c /net/ipv4/tcp_minisocks.c
/net/ipv4 /tcp_output.c /net/ipv4/tcp_timer.c etc
IP层:处理网络层的操作,网络层用struct packet_type结构表示。文件主要有:/net/ipv4/ip_forward.c ip_fragment.c ip_input.c ip_output.c etc.
数据链路层和驱动程序:每个网络设备以struct net_device表示,通用的处理在dev.c中,驱动程序都在/driver/net目录下。
2.3 socket
各种网络应用程序基本上都是通过 Linux Socket 编程接口来和内核空间的网络协议栈通信的。Linux Socket 是从 BSD Socket 发展而来的,它是 Linux 操作系统的重要组成部分之一,它是网络应用程序的基础。从层次上来说,它位于应用层,是操作系统为应用程序员提供的 API,通过它,应用程序可以访问传输层协议。
- socket 位于传输层协议之上,屏蔽了不同网络协议之间的差异
- socket 是网络编程的入口,它提供了大量的系统调用,构成了网络程序的主体
- 在Linux系统中,socket 属于文件系统的一部分,网络通信可以被看作是对文件的读取,使得我们对网络的控制和对文件的控制一样方便。
3 跟踪调试内核函数的环境
1)安装QEMU虚拟机、build-essential软件包
2)下载Linux内核压缩文件
3) 解压内核文件,进入其目录进行配置,打开debug选项,关闭KASLR设置
4)编译内核,得到bzimage压缩文件
5) 下载busybox,解压,创建文件系统根目录,创建init脚本
6)将进行socket通信的源文件client.c和server.c放入创建的系统文件目录中
7)压缩系统文件目录
8)使用QEMU虚拟机bzimage文件和系统目录压缩文件,并暂停QEMU虚拟机
9)创建新终端,启动gdb,加载vmlinux,连接QEMU虚拟机,设置断点。此时gdb所在的中断可以对内核进行断点调试
10)在QEMU中,后台启动server.c程序,配置环路地址,以环路地址为参数启动client,此时二者开始相互通信
4 send过程的Linux内核实现
client程序运行后,执行socket通信过程,使用send系统调用发送数据,依次经过应用层、传输层、网络层、数据链路层封装。
4.1 应用层
1)网络应用调用Socket API socket (int family, int type, int protocol) 创建一个 socket,该调用最终会调用 Linux system call socket() ,并最终调用 Linux Kernel 的 sock_create() 方法。该方法返回被创建好了的那个 socket 的 file descriptor。对于每一个 userspace 网络应用创建的 socket,在内核中都有一个对应的 struct socket和 struct sock。其中,struct sock 有三个队列(queue),分别是 rx , tx 和 err,在 sock 结构被初始化的时候,这些缓冲队列也被初始化完成;在收据收发过程中,每个 queue 中保存要发送或者接受的每个 packet 对应的 Linux 网络栈 sk_buffer 数据结构的实例 skb。
2)对于 TCP socket 来说,应用调用 connect()API ,使得客户端和服务器端通过该 socket 建立一个虚拟连接。在此过程中,TCP 协议栈通过三次握手会建立 TCP 连接。默认地,该 API 会等到 TCP 握手完成连接建立后才返回。在建立连接的过程中的一个重要步骤是,确定双方使用的 Maxium Segemet Size (MSS)。因为 UDP 是面向无连接的协议,因此它是不需要该步骤的。
3)应用调用 Linux Socket 的 send 或者 write API 来发出一个 message 给接收端
4) sock_sendmsg 被调用,它使用 socket descriptor 获取 sock struct,创建 message header 和 socket control message
5) _sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数。TCP 调用 tcp_sendmsg 函数, UDP调用send()/sendto()/sendmsg() 三个 system call 中的任意一个来发送 UDP message,它们最终都会调用内核中的 udp_sendmsg() 函数。
1 int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags,
2 struct sockaddr __user *addr, int addr_len)
3 {
4 ...
5 err = sock_sendmsg(sock, &msg);
6 ...
7 return err;
8 }
4.2 传输层
1)传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,主要功能包括 (1)构造 TCP segment (2)计算 checksum (3)发送回复(ACK)包 (4)滑动窗口(sliding windown)等保证可靠性的操作。
2)TCP 栈简要过程:一、tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开始 segement 发送流程。二、构造 TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从 userspace buffer 中拷贝 packet 的数据到 skb 的 buffer。三、构造 TCP header。四、计算 TCP 校验和(checksum)和 顺序号 (sequence number)。五、TCP 校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。TCP校验和覆盖 TCP 首部和 TCP 数据。六、发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程。
3)UDP 栈简要过程:udp调用栈相对tcp要简单。一、UDP 将 message 封装成 UDP 数据报。二、调用 ip_append_data() 方法将 packet 送到 IP 层进行处理。
4.3 网络层
1)网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。网络层将数据链路层提供的帧组成数据包,包中封装有网络层包头,其中含有逻辑地址信息- -源站点和目的站点地址的网络地址。其主要任务包括 (1)路由处理,即选择下一跳 (2)添加 IP header(3)计算 IP header checksum,用于检测 IP 报文头部在传播过程中是否出错 (4)可能的话,进行 IP 分片(5)处理完毕,获取下一跳的 MAC 地址,设置链路层报文头,然后转入链路层处理。
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,返回消息过长错误码。3)用 ip_finish_ouput2 设置链路层报文头了。如果,链路层报头缓存有(即hh不为空),那就拷贝到skb里。如果没,那么就调用neigh_resolve_output,使用 ARP 获取。
3)路由查询从fib_lookup函数开始,之后调用fib_table_lookup函数,函数中加锁进行同步控制,互斥访问fib_table路由表数据结构,得到的路由查询结果以fib_result数据结构返回。在fib_table_lookup中,我们可以发现,路由表中的网络地址是被字典树tire统一组织的,这使得查找最长匹配路径的效率很高。
int fib_table_lookup(struct fib_table *tb, const struct flowi4 *flp, struct fib_result *res, int fib_flags) { /* Step 1: Travel to the longest prefix match in the trie */ for (;;) { ...if (IS_LEAF(n)) goto found; n = get_child_rcu(n, index); if (unlikely(!n)) goto backtrace; } /* Step 2: Sort out leaves and begin backtracing for longest prefix */ for (;;) { if (unlikely(IS_LEAF(n))) break; while ((n = rcu_dereference(*cptr)) == NULL) { backtrace: while (!cindex) { ... } } found: ... /* Step 3: Process the leaf, if that fails fall back to backtracing */ hlist_for_each_entry_rcu(fa, &n->leaf, fa_list) { struct fib_info *fi = fa->fa_info; int nhsel, err; ... if ((BITS_PER_LONG > KEYLENGTH) || (fa->fa_slen < KEYLENGTH)) { ... goto backtrace; }
4.4 数据链路层和物理层
1)功能上,在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列。数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)。数据链路层协议的代表包括:SDLC、HDLC、PPP、STP、帧中继等。实现上,Linux 提供了一个 Network device 的抽象层,其实现在 linux/net/core/dev.c。具体的物理网络设备在设备驱动中(driver.c)需要实现其中的虚函数。Network Device 抽象层调用具体网络设备的函数。
2)物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中。在数据拷贝中,同时加入符合以太网协议的相关header,IFG、前导符和CRC。对于以太网网络,物理层发送采用CSMA/CD,即在发送过程中侦听链路冲突。一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。
5 recv过程的Linux内核实现
5.1 应用层
1)每当用户应用调用 read 或者 recvfrom 时,该调用会被映射为/net/socket.c 中的 sys_recv 系统调用,并被转化为 sys_recvfrom 调用,然后调用 sock_recgmsg 函数。
2)对于 INET 类型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法会被调用,它会调用相关协议的数据接收方法。
3)对 TCP 来说,调用 tcp_recvmsg。该函数从 socket buffer 中拷贝数据到 user buffer。
4)对 UDP 来说,从 user space 中可以调用三个 system call recv()/recvfrom()/recvmsg() 中的任意一个来接收 UDP package,这些系统调用最终都会调用内核中的 udp_recvmsg 方法。
5.2 传输层
1)传输层 TCP 处理入口在 tcp_v4_rcv 函数(位于 linux/net/ipv4/tcp ipv4.c 文件中),它会做 TCP header 检查等处理。
2)调用 _tcp_v4_lookup,查找该 package 的 open socket。如果找不到,该 package 会被丢弃。接下来检查 socket 和 connection 的状态。
3)如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。
5.3 网络层
1)IP 层的入口函数在 ip_rcv 函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。
2)ip_rcv_finish 函数会调用 ip_router_input 函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃:
3)如果是发到本机的话,调用 ip_local_deliver 函数,可能会做 de-fragment(合并多个 IP packet),然后调用 ip_local_deliver 函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而处理流程进入 TCP 栈。
4)如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用 dst_input 函数。该函数会 处理 Netfilter Hook;执行 IP fragmentation;调用 dev_queue_xmit,进入链路层处理流程。
5.4 数据链路层和物理层
1)一个 package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。
2)网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中;从数据帧中提取出一些信息,并设置 skb_buff 相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;
3)终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。
4)内核 2.5 中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:(1) 通过以前的函数netif_rx;(2)通过NAPI机制。该中断处理程序调用 Network device的 netif_rx_schedule 函数,进入软中断处理流程,再调用 net_rx_action 函数。
5)该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从 rx_ring 中被删除,进入 netif _receive_skb 处理流程。
6)netif_receive_skb 是链路层接收数据报的最后一站。它根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。
6 时序图
7 参考文献
1 https://www.cnblogs.com/jmilkfan-fanguiju/p/12789808.html#Linux__23
2 https://blog.csdn.net/cz_hyf/article/details/602802
3 https://www.cnblogs.com/my_life/articles/4691254.html