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

  • A+
所属分类:linux技术
摘要

 一、TCP/IP和网络分层介绍     1.TCP/IP概念     2.TCP/IP协议栈组成

 

目录 

一、TCP/IP和网络分层介绍

    1.TCP/IP概念

    2.TCP/IP协议栈组成

    3.OSI模型和TCP/IP模型

        1)应用层

        2)传输层

        3)网际层

        4)网络访问层

二、Socket介绍

    1.什么是socket

    2.Socket通信

    3.相关函数及作用

    4.相关数据结构

        1)sockaddr

        2)sockaddr_in

        3)struct in_addr

三、Linux内核网络结构

    1.网络结构

    2.有关的数据结构

        1)sk_buff

        2)net_device

四、send过程

    1.总体流程

        1) 应用层

        2)传输层

        3) 网络层

        4)链路层

    2.gdb调试跟踪

    3.部分源代码分析

        1)tcp_sendmsg()

        2)ip_sendmsg()

五、recv过程

    1.总体流程

        1)链路层

        2)网络层

        3)传输层

        4)应用层

    2.gdb调试跟踪

    3.部分源代码分析

        1)tcp_v4_rev()

        2)tcp_data_queue()

六、时序图

七、参考文献

 

一、TCP/IP和网络分层介绍

1.TCP/IP概念

互联网协议套件(Internet Protocol Suite,缩写IPS)是网络通信模型,以及整个网络传输协议家族,为网际网络的基础通信架构。它常通称为TCP/IP协议族,简称TCP/IP,因为该协议家族的两个核心协议:TCP(传输控制协议)和IP(网际协议),为该家族中最早通过的标准。由于在网络通讯协议普遍采用分层的结构,当多个层次的协议共同工作时,类似计算机科学中的堆栈,因此又称为TCP/IP协议栈。这些协议最早发源于美国国防部(缩写为DoD)的ARPA网项目,因此也称作DoD模型(DoD Model)。这个协议族由互联网工程任务组负责维护。

TCP/IP提供了点对点链接的机制,将资料应该如何封装、寻址、传输、路由以及在目的地如何接收,都加以标准化。它将软件通信过程抽象化为四个抽象层,采取协议堆栈的方式,分别实现出不同通信协议。协议族下的各种协议,依其功能不同,分别归属到这四个层次结构之中,常视为是简化的七层OSI模型。

2.TCP/IP协议栈组成

整个通信网络的任务,可以划分成不同的功能区块,即所谓的层级(layer),用于互联网的协议可以比照TCP/IP参考模型进行分类。TCP/IP协议栈起始于第三层协议IP(网际协议)。所有这些协议都在相应的RFC文档中讨论及标准化。重要的协议在相应的RFC文档中均标记状态:“必须”(required),“推荐”(recommended),“可选”(selective)。其他的协议还可能有“试验”(experimental)或“历史”(historic)的状态。”

3.OSI模型和TCP/IP模型

OSI模型 协议或功能物件
7.应用层 例如HTTP、SMTP、SNMP、FTP、Telnet、SIP、SSH、NFS、RTSP、XMPP、Whois、ENRP、TLS
6.表示层 例如XDR、ASN.1、NCP、TLS、ASCII
5.会话层 例如ASAP、ISO 8327 / CCITT X.225、RPC、NetBIOS、Winsock、BSD sockets、SOCKS、密码验证协议
4.传输层 例如TCP、UDP、RTP、SCTP、SPX、ATP、IL
3.网络层 例如IP、ICMP、IPX、BGP、OSPF、RIP、IGRP、EIGRP、ARP、RARP、X.25
2.数据链路层 例如以太网、令牌环、HDLC、帧中继、ISDN、ATM、IEEE 802.11、FDDI、PPP
1.物理层 例如调制解调器、无线电、光纤
TCP/IP模型 协议或功能物件
4.应用层 例如HTTP、FTP、DNS(如BGP和RIP这样的路由协议,尽管由于各种各样的原因它们分别运行在TCP和UDP上,仍然可以将它们看作网络层的一部分)
3.传输层 例如TCP、UDP、RTP、SCTP<(如OSPF这样的路由协议,尽管运行在IP上也可以看作是网络层的一部分)
2.网际层 对于TCP/IP来说这是因特网协议(IP)(如ICMP和IGMP这样的必须协议尽管运行在IP上,也仍然可以看作是网络互连层的一部分;ARP不运行在IP上)
1.网络访问层 例如以太网、Wi-Fi、MPLS等。

通常人们认为OSI模型的最上面三层(应用层、表示层和会话层)在TCP/IP组中是一个应用层。由于TCP/IP有一个相对较弱的会话层,由TCP和RTP下的打开和关闭连接组成,并且在TCP和UDP下的各种应用提供不同的端口号,这些功能能够由单个的应用程序(或者那些应用程序所使用的库)增加。与此相似的是,IP是按照将它下面的网络当作一个黑盒子的思想设计的,这样在讨论TCP/IP的时候就可以把它当作一个独立的层。

1)应用层

该层包括所有和应用程序协同工作,利用基础网络交换应用程序专用的数据的协议。 应用层是大多数普通与网络相关的程序为了通过网络与其他程序通信所使用的层。这个层的处理过程是应用特有的;数据从网络相关的程序以这种应用内部使用的格式进行传送,然后编码成标准协议的格式。

一些特定的程序视为在此层运行。它们提供服务直接支持用户应用。这些程序和它们对应的协议包括HTTP(万维网服务)、FTP(文件传输)、SMTP(电子邮件)、SSH(安全远程登录)、DNS(名称⇔IP地址寻找)以及许多其他协议。

每一个应用层(TCP/IP参考模型的最高层)协议一般都会使用到两个传输层协议之一: 面向连接的TCP传输控制协议和无连接的包传输的UDP用户数据报文协议。

2)传输层

传输层(transport layer)的协议,能够解决诸如端到端可靠性(“数据是否已经到达目的地?”)和保证数据按照正确的顺序到达这样的问题。在TCP/IP协议组中,传输协议也包括所给数据应该送给哪个应用程序。 在TCP/IP协议组中技术上位于这个层的动态路由协议通常认为是网络层的一部分;一个例子就是OSPF(IP协议89)。

TCP(IP协议6)是一个“可靠的”、面向链接的传输机制,它提供一种可靠的字节流保证数据完整、无损并且按顺序到达。TCP尽量连续不断地测试网络的负载并且控制发送数据的速度以避免网络过载。另外,TCP试图将数据按照规定的顺序发送。这是它与UDP不同之处,这在实时数据流或者路由高网络层丢失率应用的时候可能成为一个缺陷。 较新的SCTP也是一个“可靠的”、面向链接的传输机制。它是面向记录而不是面向字节的,它在一个单独的链接上提供通过多路复用提供的多个子流。它也提供多路自寻址支持,其中链接终端能够以多个IP地址表示(代表多个实体接口),这样的话即使其中一个连接失败了也不中断。它最初是为电话应用开发的(在IP上传输SS7),但是也可以用于其他的应用。

UDP(IP协议号17)是一个无链接的数据报协议。它是一个“尽力传递”(best effort)或者说“不可靠”协议——不是因为它特别不可靠,而是因为它不检查数据包是否已经到达目的地,并且不保证它们按顺序到达。如果一个应用程序需要这些特性,那它必须自行检测和判断,或者使用TCP协议。 UDP的典型性应用是如流媒体(音频和视频等)这样按时到达比可靠性更重要的应用,或者如DNS查找这样的简单查询/响应应用,如果创建可靠的链接所作的额外工作将是不成比例地大。 DCCP目前正由IETF开发。它提供TCP流动控制语义,但对于用户来说保留UDP的数据报服务模型。

3)网际层

TCP/IP协议族中的网际层(internet layer)在OSI模型中叫做网络层(network layer)。正如最初所定义的,网络层解决在一个单一网络上传输数据包的问题。类似的协议有X.25和ARPANET的Host/IMP Protocol。 随着因特网思想的出现,在这个层上添加附加的功能,也就是将数据从源网络传输到目的网络。这就牵涉到在网络组成的网上选择路径将数据包传输,也就是因特网。 在因特网协议组中,IP完成数据从源发送到目的的基本任务。IP能够承载多种不同的高层协议的数据;这些协议使用一个唯一的IP协议号进行标识。ICMP和IGMP分别是1和2。 一些IP承载的协议,如ICMP(用来发送关于IP发送的诊断信息)和IGMP(用来管理多播数据),它们位于IP层之上但是完成网络层的功能,这表明因特网和OSI模型之间的不兼容性。所有的路由协议,如BGP、OSPF、和RIP实际上也是网络层的一部分,尽管它们似乎应该属于更高的协议栈。

4)网络访问层

网络访问层实际上并不是因特网协议组中的一部分,但是它是数据包从一个设备的网络层传输到另外一个设备的网络层的方法。这个过程能够在网卡的软件驱动程序中控制,也可以在韧体或者专用芯片中控制。这将完成如添加报头准备发送、通过实体介质实际发送这样一些数据链路功能。另一端,链路层将完成数据帧接收、去除报头并且将接收到的包传到网络层。 然而,链路层并不经常这样简单。它也可能是一个虚拟专有网络(VPN)或者隧道,在这里从网络层来的包使用隧道协议和其他(或者同样的)协议组发送而不是发送到实体的接口上。VPN和通道通常预先建好,并且它们有一些直接发送到实体接口所没有的特殊特点(例如,它可以加密经过它的数据)。由于现在链路“层”是一个完整的网络,这种协议组的递归使用可能引起混淆。但是它是一个实现常见复杂功能的一个优秀方法。(尽管需要注意预防一个已经封装并且经隧道发送下去的数据包进行再次地封装和发送)。

二、Socket介绍

1.什么是socket

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部。

Socket起源于Unix,而Unix/Linux 基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式 来操作。Socket就是该模式的一个实现,Socket即是一种特殊的文件,一些Socket函数就是对其进行的操作(读/写IO、打开、关闭)

2.Socket通信

Socket保证了不同计算机之间的通信,也就是网络通信。对于网站,通信模型是服务器与客户端之间的通信。两端都建立了一个Socket对象,然后通过Socket对象对数据进行传输。通常服务器处于一个无限循环,等待客户端的连接。

如图为TCP建立Socket连接的函数调用过程:TCP/IP协议栈在Linux内核中的运行时序分析

 

3.相关函数及作用

  1. int socket(int domain, int type, int protocol):创建一个新的套接字,返回套接字描述符

  2. int bind(int sockfd,struct sockaddr * my_addr,int addrlen):为套接字指明一个本地端点地址TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号,服务器使用它来指明熟知的端口号,然后等待连接

  3. int listen(int sockfd,int input_queue_size):面向连接的服务器使用它将一个套接字置为被动模式,并准备接收传入连接。用于服务器,指明某个套接字连接是被动的

  4. int accept(int sockfd, struct sockaddr *addr, int *addrlen):获取传入连接请求,返回新的连接的套接字描述符

  5. int connect(int sockfd,struct sockaddr *server_addr,int sockaddr_len):同远程服务器建立主动连接,成功时返回0,若连接失败返回-1。

  6. int send(int sockfd, const void * data, int data_len, unsigned int flags):在TCP连接上发送数据,返回成功传送数据的长度,出错时返回-1

  7. int recv(int sockfd, void *buf, int buf_len,unsigned int flags):从TCP接收数据,返回实际接收的数据长度

  8. close(int sockfd):撤销套接字

4.相关数据结构

1)sockaddr

1 struct sockaddr { 2    unsigned short   sa_family; 3    char             sa_data[14]; 4 };

 

这是一个通用的套接字地址结构,在大多数套接字函数调用中都需要使用它。 成员字段的说明如下。sa_family包括以下可选值。每个值代表一种地址族(address family),在基于IP的情况中,都使用AF_INET。

  • AF_INET

  • AF_UNIX

  • AF_NS

  • AF_IMPLINK

sa_data长为14字节,根据地址类型解释协议特定地址。

2)sockaddr_in

1 struct sockaddr_in { 2    short int            sin_family; 3    unsigned short int   sin_port; 4    struct in_addr       sin_addr; 5    unsigned char        sin_zero[8]; 6 };

 

其中,sin_family和sockadd的sa_family一样,包括四个可选值。

sin_port是端口号,16位长,网络字节序(network byte order);sin_addr是IP地址,32位长,网络字节序(network byte order)。sin_zero,8个字节,设置为0。

3)struct in_addr

1 struct in_addr { 2    unsigned long s_addr; 3 };

 

就是一个32位的IP地址,同样是网络字节序。

关于字节序,补充一些内容:

  • Little Endian - 在该方案中,低位字节存储在起始地址(A)上,高位字节存储在下一个地址(A + 1)上。

  • Big Endian - 在该方案中,高位字节存储在起始地址(A)上,低位字节存储在下一个地址(A + 1)上。

为了允许具有不同字节顺序约定的机器相互通信,Internet协议为通过网络传输的数据指定了规范的字节顺序约定。 这称为网络字节顺序。在建立Internet套接字连接时,必须确保sockaddr_in结构的sin_port和sin_addr成员中的数据在网络字节顺序中表示。

不用担心这几个数据结构以及字节序,因为socket接口非常贴心地准备好了各种友好的接口。

  • htons() Host to Network Short

  • htonl() Host to Network Long

  • ntohl() Network to Host Long

  • ntohs() Network to Host Short

三、Linux内核网络结构

1.网络结构

Linux内核采用分层结构处理网络数据包。分层结构与网络协议的结构匹配,既能简化数据包处理流程,又便于扩展和维护。

在Linux内核中,对网络部分按照网络协议层、网络设备层、设备驱动功能层和网络媒介层的分层体系设计。网络驱动功能层主要通过网络驱动程序实现。网络设备驱动程序的主要任务如下:

  • 接收目的地为当前主机的数据包,并将其传递给网络层,之后再将其传递给传输层。

  • 传输当前主机生成的外出数据包或转发当前主机收到的数据包。

在Linux内核,所有的网络设备都被抽象为一个接口处理,该接口提供了所有的网络操作。

net_device结构表示网络设备在内核中的情况,也就是网络设备接口。网络设备接口既包括软件虚拟的网络设备接口,如环路设备,也包括了网络硬件设备,如以太网卡。Linux内核有一个dev_base的全局指针,指向一个设备链表,包括了系统内的所有网络设备。该设备链表每个节点是一个网络设备。

在net_device结构中提供了许多供系统访问和协议层调用的设备方法,包括初始化、打开关闭设备、数据包发送和接收等。

2.有关的数据结构

1)sk_buff

内核对网络数据包的处理都是基于sk_buff结构的,该结构是内核网络部分最重要的数据结构。网络协议栈中各层协议都可以通过对该结构的操作实现本层协议数据的添加或者删除。使用sk_buff结构避免了网络协议栈各层来回复制数据导致的效率低下。

sk_buff结构可以分为两个部分,一部分是存储数据包缓存,另一部分是由一组用于内核管理的指针组成。

sk_buff管理的指针最主要的是下面4个:

  • head指向数据缓冲(PackertData)的内核首地址;

  • data指向当前数据包的首地址;

  • tail指向当前数据包的尾地址;

  • end 指向数据缓冲的内核尾地址。

数据包的大小在内核网络协议栈的处理过程中会发生改变,因此data和tail指针也会不断变化,而head和tail指针是不会发生改变的。对各层设置指针的是方便了协议栈对数据包的处理。

sk_buff结构表示一个包含报头的数据包,我们一般称之为SKB(套接字缓冲区),它能够处理可变长数据,能够很容易地在数据区头尾部添加和移除数据,且尽量避免数据的复制。每一个SKB都在设备结构中标识发送报文的目的地或接收发送报文的来源地,通常每个报文使用一个SKB表示,SKB主要用于在网络驱动程序和应用程序之间传递、复制数据包。

  • 当应用程序要发送一个数据包时,数据通过系统调用提交到内核中,系统会分配一个SKB来存储数据,然后往下层传递,在传递给网络驱动后才将其释放。

  • 当网络设备接收到数据包后,同样要分配一个SKB来存储数据,然后往上传递,最终在数据复制到应用程序后才释放。

2)net_device

Linux内核中网络设备最重要的数据结构就是net_device结构了,它是网络驱动程序最重要的部分。 net_device结构保存在include/linux/netdevices.h头文件,理解该结构对理解网络设备驱动有很大帮助。

内核中所有网络设备的信息和操作都在net_device设备中,无论是注册网络设备,还是设置网络设备参数,都用到该结构。

下面是主要数据成员。

    • 设备名称

    • 总线参数

    • 协议参数

    • 链接层变量

    • 接口标志

四、send过程

1.总体流程

1) 应用层

  首先网络应用调用Socket 的API函数 socket()(该函数定义在/usr/include/sys/socket.h文件中) ,创建一个 socket(函数会调用系统调用 socket()),并最终调用内核函数的 sock_create (定义在net/socket.c)方法,成功后返回一个socket描述符。

  对于TCP,应用接着调用 connect()函数,使得客户端和服务器端通过该 socket 建立一个连接。然后可以调用send函数发出一个 message 给接收端。sock_sendmsg 被调用,调用相应协议的发送函数。

2)传输层

数据到了传输层的处理,以TCP协议为例。TCP主要处理:(1)构造 TCP segment (2)计算 checksum (3)发送回复(ACK)包 (4)滑动窗口(sliding windown)等保证可靠性的操作。

不同的协议针对的发送函数不一样,TCP调用 tcp_sendmsg 函数。

  如果是tcp协议的流程,tcp_sendmsg()的主要工作是把用户层的数据,填充到skb中。然后调用tcp_push()来发送,tcp_push函数调用tcp_write_xmit()函数,其又将调用发送函数tcp_transmit_skb,所有的SKB都经过该函数进行发送。最后进入到ip_queue_xmit到网络层。因为tcp会进行重传控制,所以有tcp_write_timer函数,进行定时。

3) 网络层

  ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,就会去选择一个路由。

  填充IP包的各个字段,比如版本、包头长度、TOS等。当报文的长度大于mtu,gso的长度不为0就会调用 ip_fragment 进行分片。ip_fragment 函数中,会检查 IP_DF 标志位,如果待分片IP数据包禁止分片,则调用 icmp_send()向发送方发送一个原因为需要分片而设置了不分片标志的目的不可达ICMP报文,并丢弃报文,即设置IP状态为分片失败,释放skb,返回消息过长错误码。

 

4)链路层

  数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。这一层数据的单位称为帧(frame)。从dev_queue_xmit函数开始,位于net/core/dev.c文件中。

2.gdb调试跟踪

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

3.部分源代码分析

1)tcp_sendmsg()

 1 int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t size)  2 {  3     ...  4     //检测三次握手是否成功  5     if (((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) &&  6         ! (tcp_passive_fastopen(sk)) {  7    8         /* 等待连接的建立,成功时返回值为0 */  9         if ((err = sk_stream_wait_connect(sk, &timeo)) != 0) 10             goto do_error; 11     } 12     ... 13 }

 

SOCK_STREAM类socket的TCP层操作函数集实例为tcp_prot,其中使用tcp_sendmsg()来发送数据。

1 struct proto tcp_prot = { 2     .name = "TCP", 3     .owner = THIS_MODULE, 4     ... 5     .sendmsg = tcp_sendmsg, 6     ... 7 };

 

2)ip_sendmsg()

 

ip_queue_xmit最终也是调用ip_local_out发送本机的数据包。

  1 int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)   2 {   3     struct sock *sk = skb->sk;   4     struct inet_sock *inet = inet_sk(sk);   5     struct ip_options_rcu *inet_opt;   6     struct flowi4 *fl4;   7     struct rtable *rt;   8     struct iphdr *iph;   9     int res;  10     /* 判断数据包是否有路由,如果已经有了,就直接跳到  11 packet_routed */  12     rcu_read_lock();  13     inet_opt = rcu_dereference(inet->inet_opt);  14     fl4 = &fl->u.ip4;  15     rt = skb_rtable(skb);  16     if (rt != NULL)  17         goto packet_routed;  18     /* 从套接字获得合法的路由(需要检查是否过期)  19  */  20     rt = (struct rtable *)__sk_dst_check(sk, 0);  21     if (rt == NULL) {  22         __be32 daddr;  23         daddr = inet->inet_daddr;  24         /* 如果有  25 IP 严格路由选项,则使用选项中的地址作为目的地址进行路由查询  26  */  27         if (inet_opt && inet_opt->opt.srr)  28             daddr = inet_opt->opt.faddr;  29         /* 进行路由查找  30  */  31         rt = ip_route_output_ports(sock_net(sk), fl4, sk,  32                      daddr, inet->inet_saddr,  33                      inet->inet_dport,  34                      inet->inet_sport,  35                      sk->sk_protocol,  36                      RT_CONN_FLAGS(sk),  37                      sk->sk_bound_dev_if);  38         if (IS_ERR(rt))  39             goto no_route;  40         /* 根据路由的接口的特性设置套接字特性  41  */  42         sk_setup_caps(sk, &rt->dst);  43     }  44     /* 给数据包设置路由  45  */  46     skb_dst_set_noref(skb, &rt->dst);  47 packet_routed:  48     /* 如果有  49 IP严格路由选项  50  */  51     if (inet_opt && inet_opt->opt.is_strictroute && fl4->daddr != rt->rt_gateway)  52         goto no_route;  53     /* 分配  54 IP首部和选项空间  55  */  56     skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));  57     /* 设置  58 IP首部位置  59  */  60     skb_reset_network_header(skb);  61     /* 得到数据包  62 IP首部的指针  63  */  64     iph = ip_hdr(skb);  65     /* 构建  66 IP首部  67  */  68     *((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));  69     /* 如不能分片,则在  70 IP首部设置  71 IP_DF标志  72  */  73     if (ip_dont_fragment(sk, &rt->dst) && !skb->local_df)  74         iph->frag_off = htons(IP_DF);  75     else  76         iph->frag_off = 0;  77     iph->ttl      = ip_select_ttl(inet, &rt->dst);  78     iph->protocol = sk->sk_protocol;  79     iph->saddr    = fl4->saddr;  80     iph->daddr    = fl4->daddr;  81     /* Transport layer set skb->h.foo itself. */  82     /* 构建  83 IP选项  84  */  85     if (inet_opt && inet_opt->opt.optlen) {  86         iph->ihl += inet_opt->opt.optlen >> 2;  87         ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0);  88     }  89     /* 选择合适的  90 IP identifier */  91     ip_select_ident_more(iph, &rt->dst, sk,  92                (skb_shinfo(skb)->gso_segs ?: 1) - 1);  93     /* 根据套接字选项,设置数据包的优先级和标记  94  */  95     skb->priority = sk->sk_priority;  96     skb->mark = sk->sk_mark;  97     /* 发送数据包  98  */  99     res = ip_local_out(skb); 100     rcu_read_unlock(); 101     return res; 102 no_route: 103     rcu_read_unlock(); 104     IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES); 105     kfree_skb(skb); 106     return -EHOSTUNREACH; 107 }

 

五、recv过程

1.总体流程

1)链路层

包到达机器的物理网卡时候触发一个中断,并将通过DMA传送到位于 linux kernel 内存中的rx_ring。中断处理程序分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中,并设置 skb_buff 相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;

然后发出一个软中断(NET_RX_SOFTIRQ,该变量定义在include/linux/interrupt.h 文件中),通知内核接收到新的数据帧。进入软中断处理流程,调用 net_rx_action 函数。包从 rx_ring 中被删除,进入 netif _receive_skb 处理流程。

netif_receive_skb根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。

 

2)网络层

网络IP层的入口函数在ip_rcv函数。

ip_rcv函数调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。

该函数首先会做包括checksum在内的各种检查,如果需要的话会做 IP defragment(分片合并),最终到达 ip_rcv_finish 函数。

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

  如果发到本机的话,调用 ip_local_deliver 函数,可能会做 de-fragment(合并多个包),并调用ip_local_deliver_finish。最后调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。如果需要转发,则进入转发流程,调用 dev_queue_xmit,进入链路层处理流程。如果不是发送到本机的话就要进行转发,则调用ip_forward转发。

3)传输层

传输层 TCP 处理入口在tcp_v4_rcv函数(位于 linux/net/ipv4/tcp_ipv4.c 文件中),首先会做一些完整性检查,发现问题直接将包丢弃。如果是tcp,则调用tcp_v4_do_rcv。

然后sk->sk_state == TCP_ESTABLISHED,调用tcp_rcv_established。

调用tcp_data_queue方法将报文放入队列中。然后用tcp_ofo_queue方法报文插入receive队列的。

4)应用层

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

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

  TCP 会调用 tcp_recvmsg。该函数从 socket buffer 中拷贝数据到buffer。

2.gdb调试跟踪

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

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

3.部分源代码分析

1)tcp_v4_rev()

该函数主要工作就是根据tcp头部信息查到处理报文的socket对象,然后检查socket状态做不同处理,我们这里是监听状态TCP_LISTEN,直接调用函数tcp_v4_do_rcv()。

  1 /*   2  *    From tcp_input.c   3  */   4     5 //网卡驱动-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv()   6 int tcp_v4_rcv(struct sk_buff *skb)   7 {   8     struct net *net = dev_net(skb->dev);   9     const struct iphdr *iph;  10     const struct tcphdr *th;  11     bool refcounted;  12     struct sock *sk;  13     int ret;  14    15     //如果不是发往本地的数据包,则直接丢弃  16     if (skb->pkt_type != PACKET_HOST)  17         goto discard_it;  18    19     /* Count it even if it's bad */  20     __TCP_INC_STATS(net, TCP_MIB_INSEGS);  21    22    23     ////包长是否大于TCP头的长度  24     if (!pskb_may_pull(skb, sizeof(struct tcphdr)))  25         goto discard_it;  26    27     //tcp头   --> 不是很懂为何老是获取tcp头  28     th = (const struct tcphdr *)skb->data;  29    30     if (unlikely(th->doff < sizeof(struct tcphdr) / 4))  31         goto bad_packet;  32       33     if (!pskb_may_pull(skb, th->doff * 4))  34         goto discard_it;  35    36     /* An explanation is required here, I think.  37      * Packet length and doff are validated by header prediction,  38      * provided case of th->doff==0 is eliminated.  39      * So, we defer the checks. */  40    41     if (skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo))  42         goto csum_error;  43       44     //得到tcp的头  --> 不是很懂为何老是获取tcp头  45     th = (const struct tcphdr *)skb->data;  46    47     //得到ip报文头  48     iph = ip_hdr(skb);  49     /* This is tricky : We move IPCB at its correct location into TCP_SKB_CB()  50      * barrier() makes sure compiler wont play fool^Waliasing games.  51      */  52     memmove(&TCP_SKB_CB(skb)->header.h4, IPCB(skb),  53         sizeof(struct inet_skb_parm));  54     barrier();  55    56     TCP_SKB_CB(skb)->seq = ntohl(th->seq);  57     TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +  58                     skb->len - th->doff * 4);  59     TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);  60     TCP_SKB_CB(skb)->tcp_flags = tcp_flag_byte(th);  61     TCP_SKB_CB(skb)->tcp_tw_isn = 0;  62     TCP_SKB_CB(skb)->ip_dsfield = ipv4_get_dsfield(iph);  63     TCP_SKB_CB(skb)->sacked     = 0;  64    65 lookup:  66     //根据源端口号,目的端口号和接收的interface查找sock对象------>先在建立连接的哈希表中查找------>如果没找到就从监听哈希表中找   67    68     //对于建立过程来讲肯是监听哈希表中才能找到  69     sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,  70                    th->dest, &refcounted);  71       72     //如果找不到处理的socket对象,就把数据报丢掉  73     if (!sk)  74         goto no_tcp_socket;  75    76 process:  77    78     //检查sock是否处于半关闭状态  79     if (sk->sk_state == TCP_TIME_WAIT)  80         goto do_time_wait;  81    82     if (sk->sk_state == TCP_NEW_SYN_RECV) {  83         struct request_sock *req = inet_reqsk(sk);  84         struct sock *nsk;  85    86         sk = req->rsk_listener;  87         if (unlikely(tcp_v4_inbound_md5_hash(sk, skb))) {  88             sk_drops_add(sk, skb);  89             reqsk_put(req);  90             goto discard_it;  91         }  92         if (unlikely(sk->sk_state != TCP_LISTEN)) {  93             inet_csk_reqsk_queue_drop_and_put(sk, req);  94             goto lookup;  95         }  96         /* We own a reference on the listener, increase it again  97          * as we might lose it too soon.  98          */  99         sock_hold(sk); 100         refcounted = true; 101         nsk = tcp_check_req(sk, skb, req, false); 102         if (!nsk) { 103             reqsk_put(req); 104             goto discard_and_relse; 105         } 106         if (nsk == sk) { 107             reqsk_put(req); 108         } else if (tcp_child_process(sk, nsk, skb)) { 109             tcp_v4_send_reset(nsk, skb); 110             goto discard_and_relse; 111         } else { 112             sock_put(sk); 113             return 0; 114         } 115     } 116     if (unlikely(iph->ttl < inet_sk(sk)->min_ttl)) { 117         __NET_INC_STATS(net, LINUX_MIB_TCPMINTTLDROP); 118         goto discard_and_relse; 119     } 120   121     if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb)) 122         goto discard_and_relse; 123   124     if (tcp_v4_inbound_md5_hash(sk, skb)) 125         goto discard_and_relse; 126   127     nf_reset(skb); 128   129     if (tcp_filter(sk, skb)) 130         goto discard_and_relse; 131      132     //tcp头部   --> 不是很懂为何老是获取tcp头 133     th = (const struct tcphdr *)skb->data; 134     iph = ip_hdr(skb); 135  136     skb->dev = NULL; 137  138     //如果socket处于监听状态 --> 我们重点关注这里 139     if (sk->sk_state == TCP_LISTEN) { 140         ret = tcp_v4_do_rcv(sk, skb); 141         goto put_and_return; 142     } 143  144     sk_incoming_cpu_update(sk); 145  146     bh_lock_sock_nested(sk); 147     tcp_segs_in(tcp_sk(sk), skb); 148     ret = 0; 149      150     //查看是否有用户态进程对该sock进行了锁定 151     //如果sock_owned_by_user为真,则sock的状态不能进行更改 152     if (!sock_owned_by_user(sk)) { 153         if (!tcp_prequeue(sk, skb)) 154             //--------------------------------------------------------> 155             ret = tcp_v4_do_rcv(sk, skb); 156     } else if (tcp_add_backlog(sk, skb)) { 157         goto discard_and_relse; 158     } 159     bh_unlock_sock(sk); 160  161 put_and_return: 162     if (refcounted) 163         sock_put(sk); 164  165     return ret; 166  167 no_tcp_socket: 168     if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) 169         goto discard_it; 170  171     if (tcp_checksum_complete(skb)) { 172 csum_error: 173         __TCP_INC_STATS(net, TCP_MIB_CSUMERRORS); 174 bad_packet: 175         __TCP_INC_STATS(net, TCP_MIB_INERRS); 176     } else { 177         tcp_v4_send_reset(NULL, skb); 178     } 179  180 discard_it: 181     /* Discard frame. */ 182     kfree_skb(skb); 183     return 0; 184  185 discard_and_relse: 186     sk_drops_add(sk, skb); 187     if (refcounted) 188         sock_put(sk); 189     goto discard_it; 190  191 do_time_wait: 192     if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { 193         inet_twsk_put(inet_twsk(sk)); 194         goto discard_it; 195     } 196  197     if (tcp_checksum_complete(skb)) { 198         inet_twsk_put(inet_twsk(sk)); 199         goto csum_error; 200     } 201     switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) { 202     case TCP_TW_SYN: { 203         struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev), 204                             &tcp_hashinfo, skb, 205                             __tcp_hdrlen(th), 206                             iph->saddr, th->source, 207                             iph->daddr, th->dest, 208                             inet_iif(skb)); 209         if (sk2) { 210             inet_twsk_deschedule_put(inet_twsk(sk)); 211             sk = sk2; 212             refcounted = false; 213             goto process; 214         } 215         /* Fall through to ACK */ 216     } 217     case TCP_TW_ACK: 218         tcp_v4_timewait_ack(sk, skb); 219         break; 220     case TCP_TW_RST: 221         tcp_v4_send_reset(sk, skb); 222         inet_twsk_deschedule_put(inet_twsk(sk)); 223         goto discard_it; 224     case TCP_TW_SUCCESS:; 225     } 226     goto discard_it; 227 }

 

2)tcp_data_queue()

tcp_data_queue作用为数据段的接收处理,其中分为多种情况:

(1) 无数据,释放skb,返回;

(2) 预期接收的数据段,a. 进行0窗口判断;b. 进程上下文,复制数据到用户空间;c. 不满足b或者b未完整拷贝此skb的数据段,则加入到接收队列;d. 更新下一个期望接收的序号;e. 若有fin标记,则处理fin;f. 乱序队列不为空,则处理乱序;g. 快速路径的检查和设置;h. 唤醒用户空间进程读取数据;

(3) 重传的数据段,进入快速ack模式,释放该skb;

(4) 窗口以外的数据段,进入快速ack模式,释放该skb;

(5) 数据段重叠,在进行0窗口判断之后,进行(2)中的加入接收队列,以及>=d的流程;

(6) 乱序的数据段,调用tcp_data_queue_ofo进行乱序数据段的接收处理;

  1 static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)   2 {   3     struct tcp_sock *tp = tcp_sk(sk);   4     bool fragstolen = false;   5     int eaten = -1;   6   7     /* 无数据 */   8     if (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq) {   9         __kfree_skb(skb);  10         return;  11     }  12  13     /* 删除路由缓存 */  14     skb_dst_drop(skb);  15  16     /* 去掉tcp首部 */  17     __skb_pull(skb, tcp_hdr(skb)->doff * 4);  18  19     tcp_ecn_accept_cwr(tp, skb);  20  21     tp->rx_opt.dsack = 0;  22  23     /*  Queue data for delivery to the user.  24      *  Packets in sequence go to the receive queue.  25      *  Out of sequence packets to the out_of_order_queue.  26      */  27     /* 预期接收的数据段 */  28     if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {  29         /* 窗口为0,不能接收数据 */  30         if (tcp_receive_window(tp) == 0)  31             goto out_of_window;  32  33         /* Ok. In sequence. In window. */  34         /* 进程上下文 */  35  36         /* 当前进程读取数据 */  37         if (tp->ucopy.task == current &&  38             /* 用户空间读取序号与接收序号一致&& 需要读取的数据不为0 */  39             tp->copied_seq == tp->rcv_nxt && tp->ucopy.len &&  40             /* 被用户空间锁定&& 无紧急数据 */  41             sock_owned_by_user(sk) && !tp->urg_data) {  42  43             /* 带读取长度和数据段长度的较小值 */  44             int chunk = min_t(unsigned int, skb->len,  45                       tp->ucopy.len);  46             /* 设置running状态 */  47             __set_current_state(TASK_RUNNING);  48  49             /* 拷贝数据 */  50             if (!skb_copy_datagram_msg(skb, 0, tp->ucopy.msg, chunk)) {  51                 tp->ucopy.len -= chunk;  52                 tp->copied_seq += chunk;  53                 /* 完整读取了该数据段 */  54                 eaten = (chunk == skb->len);  55  56                 /* 调整接收缓存和窗口 */  57                 tcp_rcv_space_adjust(sk);  58             }  59         }  60  61         /* 未拷贝到用户空间或者未拷贝完整数据段 */  62         if (eaten <= 0) {  63 queue_and_out:  64             /* 没有拷贝到用户空间,对内存进行检查 */  65             if (eaten < 0) {  66                 if (skb_queue_len(&sk->sk_receive_queue) == 0)  67                     sk_forced_mem_schedule(sk, skb->truesize);  68                 else if (tcp_try_rmem_schedule(sk, skb, skb->truesize))  69                     goto drop;  70             }  71  72             /* 添加到接收队列 */  73             eaten = tcp_queue_rcv(sk, skb, 0, &fragstolen);  74         }  75  76         /* 更新下一个期望接收的序号*/  77         tcp_rcv_nxt_update(tp, TCP_SKB_CB(skb)->end_seq);  78         /* 有数据 */  79         if (skb->len)  80             tcp_event_data_recv(sk, skb);  81  82         /* 标记有fin,则处理 */  83         if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)  84             tcp_fin(sk);  85  86         /* 乱序队列有数据,则处理 */  87         if (!RB_EMPTY_ROOT(&tp->out_of_order_queue)) {  88  89             /* 将乱序队列中的数据段转移到接收队列 */  90             tcp_ofo_queue(sk);  91  92             /* RFC2581. 4.2. SHOULD send immediate ACK, when  93              * gap in queue is filled.  94              */  95             /* 乱序数据段处理完毕,需要立即发送ack */  96             if (RB_EMPTY_ROOT(&tp->out_of_order_queue))  97                 inet_csk(sk)->icsk_ack.pingpong = 0;  98         }  99 100         if (tp->rx_opt.num_sacks) 101             tcp_sack_remove(tp); 102 103         /* 快路检查 */ 104         tcp_fast_path_check(sk); 105 106         /* 向用户空间拷贝了数据,则释放skb */ 107         if (eaten > 0) 108             kfree_skb_partial(skb, fragstolen); 109 110         /* 不在销毁状态,则唤醒进程读取数据 */ 111         if (!sock_flag(sk, SOCK_DEAD)) 112             sk->sk_data_ready(sk); 113         return; 114     } 115 116     /* 重传 */ 117     if (!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt)) { 118         /* A retransmit, 2nd most common case.  Force an immediate ack. */ 119         NET_INC_STATS(sock_net(sk), LINUX_MIB_DELAYEDACKLOST); 120         tcp_dsack_set(sk, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq); 121 122 out_of_window: 123         /* 进入快速ack模式 */ 124         tcp_enter_quickack_mode(sk); 125 126         /*  调度ack */ 127         inet_csk_schedule_ack(sk); 128 drop: 129         /* 释放skb */ 130         tcp_drop(sk, skb); 131         return; 132     } 133 134     /* Out of window. F.e. zero window probe. */ 135     /* 窗口以外的数据,比如零窗口探测报文段 */ 136     if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt + tcp_receive_window(tp))) 137         goto out_of_window; 138 139     /* 进入快速ack模式 */ 140     tcp_enter_quickack_mode(sk); 141 142     /* 数据段重叠 */ 143     if (before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) { 144         /* Partial packet, seq < rcv_next < end_seq */ 145         SOCK_DEBUG(sk, "partial packet: rcv_next %X seq %X - %Xn", 146                tp->rcv_nxt, TCP_SKB_CB(skb)->seq, 147                TCP_SKB_CB(skb)->end_seq); 148 149         tcp_dsack_set(sk, TCP_SKB_CB(skb)->seq, tp->rcv_nxt); 150 151         /* If window is closed, drop tail of packet. But after 152          * remembering D-SACK for its head made in previous line. 153          */ 154         /* 窗口为0,不能接收 */ 155         if (!tcp_receive_window(tp)) 156             goto out_of_window; 157         goto queue_and_out; 158     } 159 160     /* 接收乱序数据段 */ 161     tcp_data_queue_ofo(sk, skb); 162 }

 

 

六、时序图

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

 

七、参考文献

  1. https://zh.wikipedia.org/zh-hans/TCP/IP%E5%8D%8F%E8%AE%AE%E6%97%8F

  2. https://developer.aliyun.com/article/548918

  3. https://developer.aliyun.com/article/549169

  4. https://zhuanlan.zhihu.com/p/106271407

  5. https://zhuanlan.zhihu.com/p/109826876

  6. https://zhuanlan.zhihu.com/p/59296026

  7. https://blog.51cto.com/liucw/1221140

  8. https://blog.csdn.net/qq_34258344/article/details/105848293

  9. https://www.cnblogs.com/sammyliu/p/5225623.html

  10. https://blog.csdn.net/zhangskd/article/details/48207553

  11. https://www.cnblogs.com/wlcxsj2019/p/12098991.html

  12. https://www.cnblogs.com/wanpengcoder/p/11752133.html

 

2021-01-24 17:54:35