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

1 Linux内核任务调度系统

Linux内核的主要模块分为 存储管理CPU进程管理文件系统设备管理和驱动网络通信,以及系统的初始化(引导)系统调用等。

中断处理、softirg、tasklet、wq、内核线程等模块组成了Linux内核任务调度系统。

1.1 中断处理

中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。这种特殊的机制,使得CPU转去当前运行程序之外的代码,从而避免让高速的CPU迁就低速的外设。中断类事件可分为异步的中断和同步的异常;中断又分为可屏蔽中断和不可屏蔽中断;异常又分为处理器探测异常和编程异常;处理器探测异常又进一步分为故障、陷阱、终止。

1.2 softirq、tasklet、wq(work queue)

Linux把中断过程中的要执行的任务分为紧急、 非紧急、非紧急可延迟三类。中断处理程序把可延迟的操作内容推迟到内核的下半部分执行,在一个更合适的时机调用函数去完成这些可延迟的操作,从而达到快速响应。Linux内核提供了三种实现:软中断softirq、tasklet和工作队列work queue。

softirq和tasklet包含四种操作:初始化、激活、屏蔽、执行。软中断在Linux的softirq_vec中定义,使用open_softirq进行初始化,触发时调用raise_softirq函数。内核会每隔一段时间到达检查点,查看是否有软中断需要执行,如果有则调用do_softirq进行处理。

tasklet是I/O驱动程序实现可延迟函数的首选,建立在HI_SOFTIRQ和TASKLET_SOFTURQ等软中断之上,tasklet和高优先级的tasklet分别存放在tasklet_vec和tasklet_hi_vec数组中,由tasklet_action和tasklet_hi_action分别处理。几个tasklet可以同时与一个软中断关联,

work queue工作队列。它把工作推后交给一个内核线程执行,工作队列允许被重新调度甚至睡眠。在Linux中的数据结构是workqueue_struct和cpu_workqueue_struct。

其实softirq和taskled都属于软中断,而工作队列是和软中断无关,仅仅是内核中的一个内核线程在等待工作任务,工作队列可以发送工作任务。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

2 TCP/IP协议栈

2.1 TCP/IP协议栈简介

TCP/IP协议(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。与OSI模型不同,TCP/IP模型并不仅是理论上的网络标准,而是现有的事实模型,它在一定程度上参考了OSI模型,并根据当时的现存网络发展而来。

linux内核ipv4网络层次结构

  - BSD socket层: 处理BSD socket相关操作,每个socket在内核中以struct socket结构体现。
  - INET socket层:BSD socket是个可以用于各种网络协议的接口,而当用于tcp/ip,即建立了AF_INET形式的socket时,还需要保留些额外的参数,于是就有了struct socket结构。
  - TCP/UDP层:处理传输层的操作,传输层用struct inet_protocol和struct proto两个结构表示。
  - IP层:处理网络层的操作,网络层用struct packet_type结构表示。
  - 数据链路层和驱动程序:每个网络设备以struct net_device表示。

2.2 socket

Socket是独立于具体网络协议的网络编程接口。各种网络应用程序基本上都是通过 Linux Socket 编程接口来和内核空间的网络协议栈通信的。Linux Socket 是从 BSD Socket 发展而来的,它是 Linux 操作系统的重要组成部分之一,它是网络应用程序的基础。从层次上来说,它位于应用层,是操作系统为应用程序员提供的 API,通过它,应用程序可以访问传输层协议。

在TCP/IP协议中,Socket 位于传输层与应用层之间,能屏蔽不同网络协议之间的差异;

socket 是网络编程的入口,它提供了大量的系统调用,构成了网络程序的主体;

在Linux系统中,socket 属于文件系统的一部分,网络通信可以被看作是对文件的读取,使得我们对网络的控制和对文件控制一样方便。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

3 send过程的Linux内核实现

当一个 client/server 程序运行后,会执行socket通信过程,使用send系统调用发送数据,依次经过应用层、传输层、网络层、数据链路层封装。

3.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。

对于 TCP socket 来说,应用调用 connect()API ,使得客户端和服务器端通过该 socket 建立一个虚拟连接。在此过程中,TCP 协议栈通过三次握手会建立 TCP 连接。默认地,该 API 会等到 TCP 握手完成连接建立后才返回。在建立连接的过程中的一个重要步骤是,确定双方使用的 Maxium Segemet Size (MSS)。因为 UDP 是面向无连接的协议,因此它是不需要该步骤的。

应用调用 Linux Socket 的 send 或者 write API 来发出一个 message 给接收端;sock_sendmsg 被调用,它使用 socket descriptor 获取 sock struct,创建 message header 和 socket control message;_sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数。TCP 调用 tcp_sendmsg 函数, UDP调用send()/sendto()/sendmsg() 三个 system call 中的任意一个来发送 UDP message,它们最终都会调用内核中的 udp_sendmsg() 函数。

3.2 传输层

传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务。在传输层需要引入一个关键的数据结构sk_buff (skb)。

 一个skb就是一个发送缓冲区可发送的数据包。

传输层的主要功能包括:

  - 构造 TCP segment
  - 计算 checksum
  - 发送回复(ACK)包
  - 滑动窗口(sliding windown)等保证可靠性的操作。

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 校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。
  - 发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程。

3.3 网络层

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

其主要任务包括:

  - 路由处理,即选择下一跳;
  - 添加 IP header;
  - 计算 IP header checksum,用于检测 IP 报文头部在传播过程中是否出错;
  - IP 分片(可省略);
  - 处理完毕,获取下一跳的 MAC 地址,设置链路层报文头,然后转入链路层处理。

IP 栈基本处理过程如下:

  - ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由。
  - 填充IP包的各个字段,比如版本、包头长度、TOS、中间的一些分片等。
  - 用 ip_finish_ouput2 设置链路层报文头。如果链路层报头缓存存在,则拷贝到skb;否则就调用neigh_resolve_output,使用 ARP 获取。

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

3.4 数据链路层和物理层

功能上,在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列。数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)。数据链路层协议的代表包括:SDLC、HDLC、PPP、STP、帧中继等。实现上,Linux 提供了一个 Network device 的抽象层,其实现在 linux/net/core/dev.c。具体的物理网络设备在设备驱动中(driver.c)需要实现其中的虚函数。Network Device 抽象层调用具体网络设备的函数。

物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中。在数据拷贝中,同时加入符合以太网协议的相关header,IFG、前导符和CRC。对于以太网网络,物理层发送采用CSMA/CD,即在发送过程中侦听链路冲突。一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

4 recv过程的Linux内核实现

4.1 应用层

每当用户应用调用 read 或者 recvfrom 时,该调用会被映射为/net/socket.c 中的 sys_recv 系统调用,并被转化为 sys_recvfrom 调用,然后调用 sock_recgmsg 函数。

对于 INET 类型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法会被调用,它会调用相关协议的数据接收方法。

对 TCP 来说,调用 tcp_recvmsg。该函数从 socket buffer 中拷贝数据到 user buffer。

4.2 传输层

传输层 TCP 处理入口在 tcp_v4_rcv 函数(位于 linux/net/ipv4/tcp ipv4.c 文件中),它会做 TCP header 检查等处理。

调用 _tcp_v4_lookup,查找该 package 的 open socket。如果找不到,该 package 会被丢弃并在接下来检查 socket 和 connection 的状态。

如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。

4.3 网络层

IP 层的入口函数在 ip_rcv 函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。

ip_rcv_finish 函数会调用 ip_router_input 函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃:

如果是发到本机的话,调用 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 栈。

如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用 dst_input 函数。该函数会 处理 Netfilter Hook;执行 IP fragmentation;调用 dev_queue_xmit,进入链路层处理流程。

4.4 数据链路层和物理层

一个 package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。

网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中;从数据帧中提取出一些信息,并设置 skb_buff 相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;

终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。

内核 2.5 中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:(1) 通过以前的函数netif_rx;(2)通过NAPI机制。该中断处理程序调用 Network device的 netif_rx_schedule 函数,进入软中断处理流程,再调用 net_rx_action 函数。

该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从 rx_ring 中被删除,进入 netif _receive_skb 处理流程。

netif_receive_skb 是链路层接收数据报的最后一站。它根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

5 时序图

主要函数说明:
 1) sock_read: 初始化 msghdr 的结构类型变量msg,并且将需要接收的数据存放的地址传给msg.msg_iov->iov_base。
 2) sock_recvmsg: 调用函数指针 sock->ops->recvmsg() 完成在 INET Socket 层的数据接收过程。其中sock->ops被初始化为inet_stream_ops,其成员recvmsg对应的函数实现为inet_recvmsg()函数。
 3) sys_recv()/sys_recvfrom(): 分别对应着面向连接和面向无连接的协议两种情况。
 4) inet_recvmsg: 调用 sk->prot->recvmsg 函数完成数据接收,这个函数对于 tcp协议 便是 tcp_recvmsg。
 5) tcp_recvmsg: 从网络协议栈接收数据的动作,自上而下的触发动作一直到这个函数为止,出现了一次等待的过程。函数tcp_recvmsg可能会被动地等待在sk的接收数据队列上,也就是说,系统中肯定有其他地方会去修改这个队列使得 tcp_recvmsg 可以进行下去。入口参数 sk 是这个网络连接对应的 sock 指针,msg用于存放接收到的数据。接收数据的时候会去遍历接收队列中的数据,找到序列号合适的但读取队列为空时tcp_recvmsg就会调用 tcp_v4_do_rcv 使用 backlog 队列填充接收队列。
 6) tcp_v4_rcv: tcp_v4_rcv 被 ip_local_deliver 函数调用,是从IP层协议向 INET Socket 层提交的"数据到"请求,入口参数skb存放接收到的数据,len是接收的数据的长度,这个函数首先移动skb->data指针,让它指向tcp头,然后更新tcp层的一些数据统计,然后进行tcp的一些值的校验,再从 INET Socket 层中已经建立的 sock 结构变量中查找正在等待当前到达数据的哪一项,可能这个 sock 结构已经建立,或者还处于监听端口、等待数据连接的状态。返回的sock结构指针存放在sk中。然后根据其他进程对sk的操作情况,将skb发送到合适的位置。
 7) ip_rcv、ip_rcv_finish: 从以太网接收数据,放到 skb 里,作ip层的一些数据及选项检查,调用 ip_route_input() 做路由处理,判断是进行ip转发还是将数据传递到高一层的协议.调用skb->dst->input函数指针,这个指针的实现可能有多种情况,如果路由得到的结果说明这个数据包应该转发到其他主机,这里的input便是 ip_forward;如果数据包是给本机的,那么input指针初始化为 ip_local_deliver 函数。
 8) ip_local_deliver、ip_local_deliver_finish: 入口参数 skb 存放需要传送到上层协议的数据,从ip头中获取是否已经分拆的信息,如果已经分拆,则调用函数 ip_defrag 将数据包重组。然后通过调用 ip_prot->handler 指针调用 tcp_v4_rcv(tcp) 。ip_prot 是 inet_protocol 结构指针,是用来ip层登记协议的,比如由udp,tcp,icmp等协议。

 

posted @ 2021-01-28 12:35  Fibona  阅读(543)  评论(0)    收藏  举报