Linux TCP/IP协议栈追踪

时间:2021-01-26
本文章向大家介绍Linux TCP/IP协议栈追踪,主要包括Linux TCP/IP协议栈追踪使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Linux TCP/IP协议栈函数分析

本文主要分析recv函数与send函数的实现细节,包括相关数据结构和函数调用层次。分析的方法是自顶向下,即从应用层到物理层进行函数的分析。

分层模型

OSI模型和TCP/IP模型

ISO(International Organization for Standardization,国际标准化组织)设计了一种参考 模型,定义了组成网络的各个层。该模型由7层组成,称为OSI(Open Systems Interconnection,开放 系统互连)模型,如下图所示:

Linux中的网络模型

Linux内核网络子系统的实现与上图的分层模型相似,相关的C语言代码划分为不同层次,各层次都有明确定义的任务,各个层次只能通过明确定义的 接口与上下紧邻的层次通信。这种做法的好处在于,可以组合使用各种设备、传输机制和协议。例如, 通常的以太网卡不仅可用于建立因特网(IP)连接,还可以在其上传输其他类型的协议,如Appletalk 或IPX,而无须对网卡的设备驱动程序做任何类型的修改。

从应用层分析

套接字将UNIX隐喻“万物皆文件”应用到了网络连接上。内核与用户空间套接字之间的接口实现 在C标准库中,使用了socketcall系统调用。

socketcall充当一个多路分解器,将各种任务分配由不同的过程执行,例如打开一个套接字、 绑定或发送数据。

Linux采用了内核套接字的概念,使得与用户空间中的套接字的通信尽可能简单。对程序使用的 每个套接字来说,都对应于一个socket结构和sock结构的实例。二者分别充当向下(到内核)和向 上的(到用户空间)接口。

socket数据结构

socket数据结构定义如下:

<net.h>
struct socket
{
  socket_state 						state;
  unsigned long 					flags;
  const struct proto_ops* ops;
  strduct file* 					file;
  struct sock* 						sk;
  short 									type;
}
  • type指定协议类型

  • state表示套接字连接状态,具体如下:

    <net.h>
    typedef enum
    {
      SS_FREE=0;    		//未分配
      SS_UNCONNECTED;		//未连接到任何套接字
      SS_CONNECTING;		//处于连接过程中
      SS_CONNECTED;			//已经连接到另一个套接字
      SS_DISCONNECTING;	//处于断开连接过程中
    } socket_state;
    

    这里的状态与传输层协议在建立和关闭连接时使用的状态并无关系,只是单纯表示与应用程序相关的一般状态。

socketcall系统调用

由于socket并不能当成是传统的文件,需要拓展其bind、listen等功能,所以linux提供了socketcall系统调用。

17个套接字功能操作只对应到一个系统调用,sys_socketcall相当于一个萃取器(实现思想类似于C++中迭代器的不同tag,通过itertator_traits进行函数转发),根据不同参数选择最终要执行的函数。下图是sys_socketcall支持的17个套接字功能图:

接收数据(recv)

接收数据的函数(recvfrom,recv,readv,read)代码非常类似,处理过程中会合并起来,因此选择sys_recvfrom来讨论,代码流程图如下:

  • fget_light根据task_struct描述符表,查找对应的file。sock_from_file确定与之关联的inode,并使用SOCKET_I找到相关的套接字。
  • sock_recvvmsg调用接收数据的函数,TCP使用tcp_recvmsg,UDP使用_udp_recvmsg。
  • move_addr_to_user将数据复制到用户空间。

发送数据(send)

同上,发送数据可以选择(sendto,send,write,writev)函数,这些函数在控制流中被合并为一个。

  • 实现方法与recv函数相似,只不过mode_addr_to_kernel将用户态的数据复制到内核态。

传输层

这部分只分析TCP协议对函数细节的处理。

在网络层处理过分组之后,tcp_v4_rcv是TCP层的入口。

  • 将首部中的信息复制到套接字缓冲区的控制快后,内核通过__inet_lookup函数查找等待该分组的套接字。如果找到目标套接字,则返回一个已连接的套接字。

接收分组

在控制流到达tcp_v4_do_rcv后,会选择一条快速路径(前提是连接已经存在)而不是到函数分配器。如果分组容易处理(是否包含不常见选项),则进入快速路径,否则进入低速路径。

发送分组

TCP分组的发送,由更高层网络协议对tcp_sendmsg调用开始。

可以看到,tcp_sendmsg函数的调用是先通过应用层的发送函数调用的。也就是说系统提供给用户的recv、send函数是最顶层抽象程度最高的函数,实现必须依赖下层协议一层一层调用解析。

网络层

ip_rcv函数是网络层的入口点。与上下层协议的关系如下:

接收分组

分片合并

如果分组目的地址是本地计算机,ip_local_deliver函数开始工作。

由于IP分组可能是分片的,因此会带来一些困难。不见得一定有一个完整的分组可用。该函数的 第一项任务,就是通过ip_defrag重新组合分片分组的各个部分。

  • 内核会为每一个分组的各个分片建立一个分片缓存,同一个分组的各个分片都保存在一个独立的等待队列中,直至该分组的所有分片都到达。
  • 所有分片都进入缓存后,ip_frag_reasm将各个分片重新组合。

交付传输层

这里会返回到ip_local_deliver继续执行,分组的分片合并完成后,调用netfilter恢复在ip_local_deliver_finish函数中的处理。根据分组协议标识符选择一个传输层函数,将分组传递给该函数。

从上图函数调用关系可以看到,ip分组到来后,先从ip_local_deliver函数执行,最后交付TCP层的入口函数tcp_v4_rcv,

发送分组

内核提供了几个通过IP层发送数据的函数,ip_queue_xmit是最常使用的。流程图如下:

  • 首先查找合适的路由
  • ip_send_check为分组产生校验和,通过netfilter调用dst_output函数。通常,该函数的指针指向ip_output。

从上图可以看出,IP层的发送操作是由TCP层发起的(入口函数tcp_v4_rcv)。

转移到数据链路层

上图给出了ip_output函数的流程图,函数会先对MTU的数值和分组的长度进行比较,根据是否需要分片分成两个执行路径。

上图给出了相关函数的运行流程。

数据链路层

在内核中,每个网络设备都表示为类型为net_device的结构体。结构体的详细内容就不再赘述了。

接收分组

分组到达内核的时间是不可预测的。所有现代的设备驱动程序都使用中断来通知内核(或系统)有分组到达。网络驱动程序对特定于设备的中断设置了一个处理例程,因此每当该中断被引发时(即分组到达),内核都调用该处理程序,将数据从网卡传输到物理内存,或通知内核在一定时间后进行处理。

几乎所有的网卡都支持DMA模式,能够自行将数据传输到物理内存。但这些数据仍然 需要解释和处理。

上图给出了分组到达网络适配器(网卡)后,通过内核到达网络层函数的路径。

  • net_interrupt是设备驱动程序自定义的中断处理程序,负责确定中断是否是接收到分组发出的。
  • net_rx特定于网卡,创建一个套接字缓冲区,分组的内容从网卡传输到缓冲区(物理内存),内核会对这些分组数据分析首部
  • netif_rx将接收到的分组放置在CPU的等待队列上,退出中断上下文释放CPU。next_rx_action是软中断处理程序。

在经过软中断处理程序net_rx_action后,进入process_backlog函数,循环执行下列函数:

  1. __skb_dequeue函数从等待队列移除一个套接字缓冲区
  2. 有netif_receive_skb函数分析分组的类型,将分组传递给IP层

从上图可以看到,经过netif_receive_skb函数后,数据交给IP层的入口函数ip_rcv。

发送分组

网络层会通知链路层将分组发送,/net/core/dev.c中的dev_queue_xmit用于将分组发送到队列上,这是通过硬件设备驱动程序实现的,无需过多探究。

总结

Linux下网络层次相互联系的原理

通过上文可知:

  1. tcp_v4_rcv是传输层(TCP协议)的入口函数,负责与网络层和应用层联系
  2. ip_rcv是网络层的入口函数,负责与链路层和传输层联系

原文地址:https://www.cnblogs.com/ustcgerunze/p/14332716.html