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

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

SA20225634

周广维

 

目录

1. 调研要求

2.概述

  2.1 Linux概述 

    2.1.1中断处理

    2.1.2 软中断 

    2.1.3 tasklet 

    2.1.4 工作队列wq 

    2.1.5 内核线程 

  2.2 TCP/IP协议栈概述 

  2.3 Socket编程概述 

3. sendrecv调用流程 

  3.1应用层 

    3.1.1发送端

    3.1.2接收端

  3.2传输层 

    3.2.1发送端

    3.2.2接收端

  3.3网络层 

    3.3.1发送端

    3.3.2接收端

  3.4物理层与数据链路层

    3.4.1发送端

    3.4.2接收端

 

4. 时序图 

5. 实验总结 

 

 

1. 调研要求

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

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

(3)应该包括时序图

2.概述

2.1 Linux概述

Linux 内核主要由 5 个模块构成,它们分别是:

进程调度模块:用来负责控制进程对 CPU 资源的使用。所采取的调度策略是各进程能够公平合理地访问

CPU,同时保证内核能及时地执行硬件操作。

内存管理模块:用于确保所有进程能够安全地共享机器主内存区,同时,内存管理模块还支持虚拟内存管理方式,使得 Linux 支持进程使用比实际内存空间更多的内存容量。并可以利用文件系统把暂时不用的内存数据块会被交换到外部存储设备上去,当需要时再交换回来。

文件系统模块:用于支持对外部设备的驱动和存储。虚拟文件系统模块通过向所有的外部存储设备提供一个通用的文件接口,隐藏了各种硬件设备的不同细节。从而提供并支持与其它操作系统兼容的多种文件系统格式。

进程间通信模块:子系统用于支持多种进程间的信息交换方式。

网络接口模块 提供对多种网络通信标准的访问并支持许多网络硬件。

2.1.1中断处理

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

中断处理流程

1) 发生中断后,CPU执行异常向量vector_irq的代码;

2)在vector_irq里面,最终会调用中断处理C程序总入口函数asm_do_IRQ();

3asm_do_IRQ()根据中断号调用irq_des[NR_IRQS]数组中的对应数组项中的handle_irq();

4handle_irq()会使用chip的成员函数来设置硬件,例如清除中断,禁止中断,重新开启中断等;

5handle_irq逐个调用用户在action链表中注册的处理函数。

可见,中断体系结构的初始化,就是构造irq_desc[NR_IRQS]这个数据结构;用户注册中断就是构造action链表;用户卸载中断就是从action链表中去除对应的项

2.1.2 软中断

对于中断处理而言,linux将其分成了两个部分,一个叫做中断handlertop half),是全程关闭中断的,另外一部分是deferable taskbottom half),属于不那么紧急需要处理的事情。在执行bottom half的时候,是开中断的。有多种bottom half的机制,例如:softirqtaskletworkqueue或是直接创建一个kernel thread来执行bottom half

软中断的执行既可以守护进程中执行,也可以在中断的退出阶段执行。实际上,软中断更多的是在中断的退出阶段执行(irq_exit),以便达到更快的响应,加入守护进程机制,只是担心一旦有大量的软中断等待执行,会使得内核过长地留在中断上下文中。

irq_exit的部分:

void irq_exit(void)

{

        ......

sub_preempt_count(IRQ_EXIT_OFFSET);

if (!in_interrupt() && local_softirq_pending())

invoke_softirq();

        ......

}

如果中断发生嵌套,in_interrupt()保证了只有在最外层的中断的irq_exit阶段,invoke_interrupt才会被调用,当然,local_softirq_pending也会实现判断当前cpu有无待决的软中断。代码最终会进入__do_softirq中,内核会保证调用__do_softirq时,本地cpu的中断处于关闭状态,进入__do_softirq

asmlinkage void __do_softirq(void)

{

        ......

pending = local_softirq_pending();

__local_bh_disable((unsigned long)__builtin_return_address(0),

SOFTIRQ_OFFSET);

restart:

/* Reset the pending bitmask before enabling irqs */

set_softirq_pending(0);

local_irq_enable();

h = softirq_vec;

do {

if (pending & 1) {

                ......

trace_softirq_entry(vec_nr);

h->action(h);

trace_softirq_exit(vec_nr);

                        ......

}

h++;

pending >>= 1;

} while (pending);

local_irq_disable();

pending = local_softirq_pending();

if (pending && --max_restart)

goto restart;

if (pending)

wakeup_softirqd();

lockdep_softirq_exit();

__local_bh_enable(SOFTIRQ_OFFSET);

}

2.1.3 tasklet

由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。它具有以下特性:
a)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
b)多个不同类型的tasklet可以并行在多个CPU上。
c)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择。也就是说tasklet是软中断的一种特殊用法,即延迟情况下的串行执行。

2.1.4 工作队列wq

 Linux中的Workqueue机制就是为了简化内核线程的创建。通过调用workqueue的接口就能创建内核线程。并且可以根据当前系统CPU的个数创建线程的数量,使得线程处理的事务能够并行化。workqueue是内核中实现简单而有效的机制,他显然简化了内核daemon的创建,方便了用户的编程.

      工作队列(workqueue)是另外一种将工作推后执行的形式.工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。最重要的就是工作队列允许被重新调度甚至是睡眠。

2.1.5 内核线程

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

他们执行下列任务:

(1)周期性地将修改的内存页与页来源块设备同步

(2)如果内存页很少使用,则写入交换区

(3)管理延时动作, 如2号进程接手内核进程的创建

(4)实现文件系统的事务日志

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

(1)线程启动后一直等待,直至内核请求线程执行某一特定操作。

(2)线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制时采取行动。

内核线程由内核自身生成,其特点在于它们在CPU的管态执行,而不是用户态。它们只可以访问虚拟地址空间的内核部分(高于TASK_SIZE的所有地址),但不能访问用户空间

2.2 TCP/IP协议栈概述

标准TCP/IP协议是用于计算机通信的一组协议,通常被称为TCP/IP协议栈,以它为基础组建的互联网是目前国际上规模最大的计算机网络。正因为互联网的广泛应用,使得TCP/IP成为了事实上的网络标准。

下面是OSI模型和TCP/IP协议模型的对比。

 

 

 

 

TCP/IP协议模型分层

1)网络接口层

TCP/IP协议模型的基层,负责数据帧的发送和接收。对应OSI模型中的物理层和数据链路层,是TCP/IP的最底层,不过通常在描述TCP/IP模型时还是会划分具体为物理层(PHY)和数据链路层(MAC)

2)网络层

  通过互联协议将数据包封装成互联网数据包,并运行必要的路由算法。这里有4种互联协议。

  (a)网际协议IP:负责在主机和网络之间的路径寻址和数据包路由。

  (b)地址解析协议ARP:获得同一物理网络中的主机硬件地址。

  (c)网际控制消息协议ICMP:发送消息,并报告有关数据包的传送错误。

  (d)互联组管理协议IGMP:用来实现本地多路广播路由器报告。

3)传输层

  传输协议在主机之间提供通信会话。传输协议的选择根据数据传输方式而定。主要有以下2种传输协议:

  (a)传输控制协议TCP:为应用程序提供可靠的通信连接,适用于要求得到响应的应用程序。

  (b)用户数据包协议UDP:提供无连接通信,且不对传输包进行可靠性确认。

4)应用层

  应用程序通过这一层访问网络,主要包括常见的FTPHTTPDNSTELNET协议。

2.3 Socket编程概述

  socket英文为插座的意思,也就是为用户提供了一个接入某个链路的接口。而在计算机网络中,一个IP地址标识唯一一台主机,而一个端口号标识着主机中唯一一个应用进程,因此“IP+端口号就可以称之为socket

    两个主机的进程之间要通信,就可以各自建立一个socket,其实可以看做各自提供出来一个“插座”,然后通过连接上“插座”的两头也就是由这两个socket组成的socket pair就标识唯一一个连接,以此来表示网络连接中一对一的关系。

2.3.1 socket()函数

int socket(int domain, int type, int protocol);

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INETAF_INET6AF_LOCAL(或称AF_UNIXUnixsocket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

type:指定socket类型。常用的socket类型有,SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET

protocol:指定协议。常用的协议有,IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address familyAF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()listen()时系统会自动随机分配一个端口。

2.3.2 bind()函数

正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INETAF_INET6就是把一个ipv4ipv6地址和端口号组合赋给socket

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数的三个参数分别为:

(1)sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socketbind()函数就是将给这个描述字绑定一个名字。

(2)addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是: 

struct sockaddr_in {

    sa_family_t    sin_family; /* address family: AF_INET */

    in_port_t      sin_port;   /* port in network byte order */

    struct in_addr sin_addr;   /* internet address */

};

/* Internet address. */

struct in_addr {

    uint32_t       s_addr;     /* address in network byte order */

};

 

ipv6对应的是: 

struct sockaddr_in6 {

    sa_family_t     sin6_family;   /* AF_INET6 */

    in_port_t       sin6_port;     /* port number */

    uint32_t        sin6_flowinfo; /* IPv6 flow information */

    struct in6_addr sin6_addr;     /* IPv6 address */

    uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */

};

 

struct in6_addr {

    unsigned char   s6_addr[16];   /* IPv6 address */

};

Unix域对应的是: 

#define UNIX_PATH_MAX    108

struct sockaddr_un {

    sa_family_t sun_family;               /* AF_UNIX */

    char        sun_path[UNIX_PATH_MAX];  /* pathname */

};

(3)addrlen:对应的是地址的长度。

通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

注意在将一个地址绑定到socket的时候,先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian

主机字节序就是常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-EndianLittle-Endian的定义如下:

(1)Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

(2Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

网络字节序:4个字节的32 bit值以下面的次序传输:首先是07bit,其次815bit,然后1623bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

2.3.3 listen()connect()函数

如果作为一个服务器,在调用socket()bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

int listen(int sockfd, int backlog);

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

2.3.4 accept()函数

TCP服务器端依次调用socket()bind()listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

注:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

2.3.5 read()write()等函数

Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。
write() 的原型为:

ssize_t write(int fd, const void *buf, size_t nbytes);

fd为要写入的文件的描述符,buf为要写入的数据的缓冲区地址,nbytes为要写入的数据的字节数。

size_t是通过 typedef 声明的 unsigned int 类型;ssize_t "size_t" 前面加了一个"s",代表 signed,即 ssize_t 是通过 typedef 声明的 signed int 类型。

write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1
read() 的原型为:

ssize_t read(int fd, void *buf, size_t nbytes);

fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。
read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1

2.3.6 close()函数

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

#include <unistd.h>

int close(int fd);

close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为readwrite的第一个参数。

close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

2.3.7 连接的建立(三次握手)

使用 connect() 建立连接时,客户端和服务器端会相互发送三个数据包:

 

 

 


客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求。
这个时候,客户端开始发起请求:
(1)当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN-SEND状态。
(2)服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包。
服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系。
服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充“确认号(Ack)”字段。
服务器将数据包发出,进入SYN-RECV状态。
(3)客户端收到数据包,检测到已经设置了 SYN ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。
接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认号(Ack)”字段。
客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。
(4)服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。
至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来就可以收发数据了。

2.3.8 连接的断开(四次握手)

 

 

 

 

建立连接后,客户端和服务器都处于ESTABLISED状态。这时,客户端发起断开连接的请求:
1)客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN Finish 的缩写,表示完成任务需要断开连接。
2)服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。
3)客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包。
4) 等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接。然后进入LAST_ACK状态。
5)客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接。然后进入TIME_WAIT状态。
6) 服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入CLOSED状态。

2.3.9源码

客户端client源码:

#include <stdio.h>     /* perror */

#include <stdlib.h>    /* exit */

#include <sys/types.h> /* WNOHANG */

#include <sys/wait.h>  /* waitpid */

#include <string.h>    /* memset */

#include <sys/time.h>

#include <sys/types.h>

#include <unistd.h>

#include <fcntl.h>

#include <sys/socket.h>

#include <errno.h>

#include <arpa/inet.h>

#include <netdb.h> /* gethostbyname */

#define true 1

#define false 0

#define PORT 3490       /* Server的端口 */

#define MAXDATASIZE 100 /* 一次可以读的最大字节数 */

 

int main(int argc, char *argv[])

{

    int sockfd, numbytes;

    char buf[MAXDATASIZE];

    struct hostent *he;            /* 主机信息 */

    struct sockaddr_in server_addr; /* 对方地址信息 */

    if (argc != 2)

    {

        fprintf(stderr, "usage: client hostname\n");

        exit(1);

    }

    /* get the host info */

    if ((he = gethostbyname(argv[1])) == NULL)

    {

        /* 注意:获取DNS信息时,显示出错需要用herror而不是perror */

        /* herror 在新的版本中会出现警告,已经建议不要使用了 */

        perror("gethostbyname");

        exit(1);

    }

 

    if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)

    {

        perror("socket");

        exit(1);

    }

    server_addr.sin_family = AF_INET;

    server_addr.sin_port = htons(PORT); /* short, NBO */

    server_addr.sin_addr = *((struct in_addr *)he->h_addr_list[0]);

    memset(&(server_addr.sin_zero), 0, 8); /* 其余部分设成0 */

 

    if (connect(sockfd, (struct sockaddr *)&server_addr,

                sizeof(struct sockaddr)) == -1)

    {

        perror("connect");

        exit(1);

    }

 

    if ((numbytes = recv(sockfd, buf, MAXDATASIZE, 0)) == -1)

    {

        perror("recv");

        exit(1);

    }

 

    buf[numbytes] = '\0';

    printf("Received: %s", buf);

    close(sockfd);

 

    return true;

}

 

服务端server源码:

#include <stdio.h>     /* perror */

#include <stdlib.h>    /* exit */

#include <sys/types.h> /* WNOHANG */

#include <sys/wait.h>  /* waitpid */

#include <string.h>    /* memset */

#include <sys/time.h>

#include <sys/types.h>

#include <unistd.h>

#include <fcntl.h>

#include <sys/socket.h>

#include <errno.h>

#include <arpa/inet.h>

#include <netdb.h> /* gethostbyname */

#define true 1

#define false 0

#define MYPORT 3490 /* 监听的端口 */

#define BACKLOG 10  /* listen的请求接收队列长度 */

 

int main()

{

    int sockfd, new_fd;            /* 监听端口,数据端口 */

    struct sockaddr_in sa;         /* 自身的地址信息 */

    struct sockaddr_in their_addr; /* 连接对方的地址信息 */

    unsigned int sin_size;

 

    if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)

    {

        perror("socket");

        exit(1);

    }

 

    sa.sin_family = AF_INET;

    sa.sin_port = htons(MYPORT);     /* 网络字节顺序 */

    sa.sin_addr.s_addr = INADDR_ANY; /* 自动填本机IP */

    memset(&(sa.sin_zero), 0, 8);    /* 其余部分置0 */

 

    if (bind(sockfd, (struct sockaddr *)&sa, sizeof(sa)) == -1)

    {

        perror("bind");

        exit(1);

    }

 

    if (listen(sockfd, BACKLOG) == -1)

    {

        perror("listen");

        exit(1);

    }

 

    /* 主循环 */

    while (1)

    {

        sin_size = sizeof(struct sockaddr_in);

        new_fd = accept(sockfd,

                        (struct sockaddr *)&their_addr, &sin_size);

        if (new_fd == -1)

        {

            perror("accept");

            continue;

        }

 

        printf("Got connection from %s\n",

               inet_ntoa(their_addr.sin_addr));

        if (fork() == 0)

        {

            /* 子进程 */

            if (send(new_fd, "Hello, world!\n", 14, 0) == -1)

                perror("send");

            close(new_fd);

            exit(0);

        }

 

        close(new_fd);

 

        /*清除所有子进程 */

        while (waitpid(-1, NULL, WNOHANG) > 0)

            ;

    }

    close(sockfd);

    return true;

}

2.3.10一个例子

实例:实现服务端和客户端的hi/hello对话

 

 

 

 

3 .sendrecv调用流程

3.1应用层

3.1.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 来说,userspace 应用可以调用send()/ sendto()/ sendmsg() 三个 system call 中的任意一个来发送 UDP message,它们最终都会调用内核中的 udp_sendmsg() 函数。

源代码:

调用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构建完这些后,调用sock_sendmsg继续执行发送流程,传入参数为struct msghdr和数据的长度。忽略中间的一些不重要的细节,sock_sendmsg继续调用sock_sendmsg()sock_sendmsg()最后调用struct socket->ops->sendmsg,即对应套接字类型的sendmsg()函数,所有的套接字类型的sendmsg()函数都是 sock_sendmsg,该函数首先检查本地端口是否已绑定,无绑定则执行自动绑定,而后调用具体协议的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);

3.1.2 接收端

调用__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来接收数据,整个函数实际调用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags)

int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
        int flags, int *addr_len)
{
    ......
    if (sk_can_busy_loop(sk)&&skb_queue_empty (&sk->sk_receive_queue) &&(sk->sk_state == TCP_ESTABLISHED))
        sk_busy_loop(sk, nonblock);
    lock_sock(sk);
    .....
        if (unlikely(tp->repair)) {
        err = -EPERM;
        if (!(flags & MSG_PEEK))
            goto out;
        if (tp->repair_queue == TCP_SEND_QUEUE)
            goto recv_sndq;
        err = -EINVAL;
        if (tp->repair_queue == TCP_NO_QUEUE)
            goto out;
    ......
        last = skb_peek_tail(&sk->sk_receive_queue);
        skb_queue_walk(&sk->sk_receive_queue, skb) {
            last = skb;
    ......
            if (!(flags & MSG_TRUNC)) {
            err = skb_copy_datagram_msg(skb, offset, msg, used);
            if (err) {
                /* Exception. Bailout! */
                if (!copied)
                    copied = -EFAULT;
                break;
            }
        }
        *seq += used;
        copied += used;
        len -= used;
        tcp_rcv_space_adjust(sk);

这里共维护了三个队列:prequeuebacklogreceive_queue,分别为预处理队列,后备队列和接收队列,在连接建立后,若没有数据到来,接收队列为空,进程会在sk_busy_loop函数内循环等待,直到接收队列不为空,并调用函数数skb_copy_datagram_msg将接收到的数据拷贝到用户态,实际调用的是__skb_datagram_iter

int __skb_datagram_iter(const struct sk_buff *skb, int offset,
            struct iov_iter *to, int len, bool fault_short,
            size_t (*cb)(const void *, size_t, void *, struct iov_iter *),
            void *data)
{
    int start = skb_headlen(skb);
    int i, copy = start - offset, start_off = offset, n;
    struct sk_buff *frag_iter;
    /* 拷贝tcp头部 */
    if (copy > 0) {
        if (copy > len)
            copy = len;
        n = cb(skb->data + offset, copy, data, to);
        offset += n;
        if (n != copy)
            goto short_copy;
        if ((len -= copy) == 0)
            return 0;
    }

    /* 拷贝数据部分 */
    for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
        int end;
        const skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
        WARN_ON(start > offset + len);
        end = start + skb_frag_size(frag);
        if ((copy = end - offset) > 0) {
            struct page *page = skb_frag_page(frag);
            u8 *vaddr = kmap(page);
            if (copy > len)
                copy = len;
            n = cb(vaddr + frag->page_offset +
                offset - start, copy, data, to);
            kunmap(page);
            offset += n;
            if (n != copy)
                goto short_copy;
            if (!(len -= copy))
                return 0;
        }
        start = end;
    }

3.2传输层

传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,主要功能包括 1)构造 TCP segment (2)计算 checksum (3)发送回复(ACK)包 4)滑动窗口(sliding

windown)等保证可靠性的操作。

TCP 栈简要过程:

1 tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开

始 segement 发送流程。

2 构造 TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从

userspace buffer 中拷贝 packet 的数据到 skb 的 buffer

3)构造 TCP header

4)计算 TCP 校验和(checksum)和 顺序号 sequence number)。

TCP 校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。TCP校验和覆盖 TCP 首部和 TCP 数据。TCP的校验和是必需的。

5)发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程。

UDP 栈简要过程:

1UDP 将 message 封装成 UDP 数据报

2)调用 ip_append_data() 方法将 packet 送到 IP 层进行处理。

3.2.1 发送端

根据我们对应用层的追查可以发现,传输层也是先调用send()->sendto()->sys_sento->sock_sendmsg->sock_sendmsg_nosec,

在应用层调用的是inet_sendmsg函数,在传输层根据后面的断点可以知道,调用的是sock->ops-sendmsg这个函数。而sendmsg为一个宏,调用的是tcp_sendmsg,如下;

struct proto tcp_prot = {

    .name            = "TCP",

    .owner            = THIS_MODULE,

    .close            = tcp_close,

    .pre_connect        = tcp_v4_pre_connect,

    .connect        = tcp_v4_connect,

    .disconnect        = tcp_disconnect,

    .accept            = inet_csk_accept,

    .ioctl            = tcp_ioctl,

    .init            = tcp_v4_init_sock,

    .destroy        = tcp_v4_destroy_sock,

    .shutdown        = tcp_shutdown,

    .setsockopt        = tcp_setsockopt,

    .getsockopt        = tcp_getsockopt,

    .keepalive        = tcp_set_keepalive,

    .recvmsg        = tcp_recvmsg,

    .sendmsg        = tcp_sendmsg,

    ......

tcp_sendmsg实际上调用的是

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

这个函数如下:

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

{

    struct tcp_sock *tp = tcp_sk(sk);/*进行了强制类型转换*/

    struct sk_buff *skb;

    flags = msg->msg_flags;

    ......

        if (copied)

            tcp_push(sk, flags & ~MSG_MORE, mss_now,

                 TCP_NAGLE_PUSH, size_goal);

}

 

tcp_sendmsg_locked中,完成的是将所有的数据组织成发送队列,这个发送队列是struct sock结构中的一个域sk_write_queue,这个队列的每一个元素是一个skb,里面存放的就是待发送的数据。然后调用了tcp_push()函数。结构体struct sock如下:

struct sock{

    ...

    struct sk_buff_head    sk_write_queue;/*指向skb队列的第一个元素*/

    ...

    struct sk_buff    *sk_send_head;/*指向队列第一个还没有发送的元素*/

}

tcp协议的头部有几个标志字段:URGACKRSHRSTSYNFIN,tcp_push中会判断这个skb的元素是否需要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 (!skb)

        return;

    if (!(flags & MSG_MORE) || forced_push(tp))

        tcp_mark_push(tp, skb);

 

    tcp_mark_urg(tp, flags);

 

    if (tcp_should_autocork(sk, skb, size_goal)) {

        /* avoid atomic op if TSQ_THROTTLED bit is already set */

        if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) {

            NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);

            set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags);

        }

        /* It is possible TX completion already happened

         * before we set TSQ_THROTTLED.

         */

        if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize)

            return;

    }

 

    if (flags & MSG_MORE)

        nonagle = TCP_NAGLE_CORK;

    __tcp_push_pending_frames(sk, mss_now, nonagle);

}

tcp_mark_push如下:

static inline void tcp_mark_push(struct tcp_sock *tp, struct sk_buff *skb)

{

    TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;

    tp->pushed_seq = tp->write_seq;

}

...#define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0]))

...

struct sk_buff {

    ...    

    char            cb[48] __aligned(8);

    ...

struct tcp_skb_cb {

    __u32        seq;        /* Starting sequence number    */

    __u32        end_seq;    /* SEQ + FIN + SYN + datalen    */

    __u8        tcp_flags;    /* tcp头部标志,位于第13个字节tcp[13])    */

    ......

};

然后,tcp_push调用__tcp_push_pending_frames(sk, mss_now, nonagle)函数发送数据:

void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,

                   int nonagle)

{

 

    if (tcp_write_xmit(sk, cur_mss, nonagle, 0,

               sk_gfp_mask(sk, GFP_ATOMIC)))

        tcp_check_probe_timer(sk);

}

它调用了tcp_write_xmit函数来发送数据:

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,

               int push_one, gfp_t gfp)

{

    struct tcp_sock *tp = tcp_sk(sk);

    struct sk_buff *skb;

    unsigned int tso_segs, sent_pkts;

    int cwnd_quota;

    int result;

    bool is_cwnd_limited = false, is_rwnd_limited = false;

    u32 max_segs;

    /*统计已发送的报文总数*/

    sent_pkts = 0;

    ......

    /*若发送队列未满,则准备发送报文*/

    while ((skb = tcp_send_head(sk))) {

        unsigned int limit;

 

        if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {

            /* "skb_mstamp_ns" is used as a start point for the retransmit timer */

            skb->skb_mstamp_ns = tp->tcp_wstamp_ns = tp->tcp_clock_cache;

            list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue);

            tcp_init_tso_segs(skb, mss_now);

            goto repair; /* Skip network transmission */

        }

 

        if (tcp_pacing_check(sk))

            break;

        tso_segs = tcp_init_tso_segs(skb, mss_now);

        BUG_ON(!tso_segs);

        /*检查发送窗口的大小*/

        cwnd_quota = tcp_cwnd_test(tp, skb);

        if (!cwnd_quota) {

            if (push_one == 2)

                /* Force out a loss probe pkt. */

                cwnd_quota = 1;

            else

                break;

        }

        if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {

            is_rwnd_limited = true;

            break;

        ......

        limit = mss_now;

        if (tso_segs > 1 && !tcp_urg_mode(tp))

            limit = tcp_mss_split_point(sk, skb, mss_now,

                            min_t(unsigned int,

                              cwnd_quota,

                              max_segs),

                            nonagle);

        if (skb->len > limit &&

            unlikely(tso_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE,

                      skb, limit, mss_now, gfp)))

            break;

        if (tcp_small_queue_check(sk, skb, 0))

            break;

 

        if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))

            break;

    ......

tcp_write_xmit位于tcpoutput.c中,它实现了tcp的拥塞控制,然后调用了tcp_transmit_skb(sk, skb, 1, gfp)传输数据,实际上调用的是__tcp_transmit_skb

static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,

                  int clone_it, gfp_t gfp_mask, u32 rcv_nxt)

{

    

    skb_push(skb, tcp_header_size);

    skb_reset_transport_header(skb);

    ......

    /* 构建TCP头部和校验和 */

    th = (struct tcphdr *)skb->data;

    th->source        = inet->inet_sport;

    th->dest        = inet->inet_dport;

    th->seq            = htonl(tcb->seq);

    th->ack_seq        = htonl(rcv_nxt);

    tcp_options_write((__be32 *)(th + 1), tp, &opts);

    skb_shinfo(skb)->gso_type = sk->sk_gso_type;

    if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {

        th->window      = htons(tcp_select_window(sk));

        tcp_ecn_send(sk, skb, th, tcp_header_size);

    } else {

        /* RFC1323: The window in SYN & SYN/ACK segments

         * is never scaled.

         */

        th->window    = htons(min(tp->rcv_wnd, 65535U));

    }

    ......

    icsk->icsk_af_ops->send_check(sk, skb);

 

    if (likely(tcb->tcp_flags & TCPHDR_ACK))

        tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt);

    if (skb->len != tcp_header_size) {

        tcp_event_data_sent(tp, sk);

        tp->data_segs_out += tcp_skb_pcount(skb);

        tp->bytes_sent += skb->len - tcp_header_size;

    }

    if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)

        TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,

                  tcp_skb_pcount(skb));

 

    tp->segs_out += tcp_skb_pcount(skb);

    /* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */

    skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb);

    skb_shinfo(skb)->gso_size = tcp_skb_mss(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)));

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

    ......

}

3.2.2 接收端

recv函数与send类似,调用的是__sys_recvfrom,整个函数的调用路径与send类似:

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来接收数据,整个函数实际调用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);,同样,调用的其实是tcp_rcvmsg

tcp_rcvmsg函数如下:

int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,

        int flags, int *addr_len)

{

    ......

    if (sk_can_busy_loop(sk) && skb_queue_empty(&sk->sk_receive_queue) &&

        (sk->sk_state == TCP_ESTABLISHED))

        sk_busy_loop(sk, nonblock);

 

    lock_sock(sk);

    .....

        if (unlikely(tp->repair)) {

        err = -EPERM;

        if (!(flags & MSG_PEEK))

            goto out;

 

        if (tp->repair_queue == TCP_SEND_QUEUE)

            goto recv_sndq;

 

        err = -EINVAL;

        if (tp->repair_queue == TCP_NO_QUEUE)

            goto out;

    ......

        last = skb_peek_tail(&sk->sk_receive_queue);

        skb_queue_walk(&sk->sk_receive_queue, skb) {

            last = skb;

    ......

            if (!(flags & MSG_TRUNC)) {

            err = skb_copy_datagram_msg(skb, offset, msg, used);

            if (err) {

                /* Exception. Bailout! */

                if (!copied)

                    copied = -EFAULT;

                break;

            }

        }

 

        *seq += used;

        copied += used;

        len -= used;

 

        tcp_rcv_space_adjust(sk);

    

实际调用__skb_datagram_iter,同样用了struct msghdr *msg来实现。__skb_datagram_iter函数如下:

int __skb_datagram_iter(const struct sk_buff *skb, int offset,

            struct iov_iter *to, int len, bool fault_short,

            size_t (*cb)(const void *, size_t, void *, struct iov_iter *),

            void *data)

{

    int start = skb_headlen(skb);

    int i, copy = start - offset, start_off = offset, n;

    struct sk_buff *frag_iter;

    /* 拷贝tcp头部 */

    if (copy > 0) {

        if (copy > len)

            copy = len;

        n = cb(skb->data + offset, copy, data, to);

        offset += n;

        if (n != copy)

            goto short_copy;

        if ((len -= copy) == 0)

            return 0;

    }

    /* 拷贝数据部分 */

    for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {

        int end;

        const skb_frag_t *frag = &skb_shinfo(skb)->frags[i];

        WARN_ON(start > offset + len);

        end = start + skb_frag_size(frag);

        if ((copy = end - offset) > 0) {

            struct page *page = skb_frag_page(frag);

            u8 *vaddr = kmap(page);

            if (copy > len)

                copy = len;

            n = cb(vaddr + frag->page_offset +

                offset - start, copy, data, to);

            kunmap(page);

            offset += n;

            if (n != copy)

                goto short_copy;

            if (!(len -= copy))

                return 0;

        }

        start = end;

    }

拷贝完成后整个接收的过程完成。

3.3网络层

3.3.1 发送端

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

(1)路由处理,即选择下一跳

(2)添加 IP header

(3)计算 IP header checksum,用 于检测 IP 报文头部在传播过程中是否出错

(4)可能的话,进行 IP 分片

(5)处理完毕,获取下一跳的

MAC 地址,设置链路层报文头,然后转入链路层处理。

IP 栈基本处理过程如下:

1)首先,ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用

ip_route_output()选择一个路由。

2)接着,填充IP包的各个字段,比如版本、包头长度、TOS等。

3)中间的一些分片等,可参阅相关文档。基本思想是,当报文的长度大于mtu,gso的长度不为0就会调用 ip_fragment 进行分片,否则就会调用ip_finish_output2把数据发送出去。ip_fragment 函数

中,会检查 IP_DF 标志位,如果待分片IP数据包禁止分片,则调用 icmp_send()向发送方发送一个原因为需要分片而设置了不分片标志的目的不可达ICMP报文,并丢弃报文,即设置IP状态为分片失

败,释放skb,返回消息过长错误码。

4)接下来就用 ip_finish_ouput2 设置链路层报文头了。如果,链路层报头缓存有(即hh不为空),那就拷贝到skb里。如果没,那么就调用neigh_resolve_output,使用 ARP 获取。

下面为相关函数:

函数ip_queue_xmit:

int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
    return __ip_queue_xmit(sk,skb,fl,iner)sk(sk)-->tos);
}

函数ip_queue_xmit调用函数__ip_queue_xmit

/* Note: skb->sk can be different from sk, in case of tunnels */
int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl,
            __u8 tos)
{
    struct inet_sock *inet = inet_sk(sk);
    struct net *net = sock_net(sk);
    struct ip_options_rcu *inet_opt;
    struct flowi4 *fl4;
    struct rtable *rt;
    struct iphdr *iph;
    int res;
    /* Skip all of this if the packet is already routed,
     * f.e. by something like SCTP.
     */
    rcu_read_lock();
    inet_opt = rcu_dereference(inet->inet_opt);
    fl4 = &fl->u.ip4;
    //获skb中的路由
    rt = skb_rtable(skb);
    if (rt)
        goto packet_routed;
    /* Make sure we can route this packet. */
    rt = (struct rtable *)__sk_dst_check(sk, 0);
    if (!rt) {
        __be32 daddr;

调用 ip_local_out得到返回值res

int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    int err;
    err = __ip_local_out(net, sk, skb);
    if (likely(err == 1))
        err = dst_output(net, sk, skb);
    return err;

ip_local_out函数内部调用__ip_local_out

int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    struct iphdr *iph = ip_hdr(skb);
    iph->tot_len = htons(skb->len);
    ip_send_check(iph);
    /* if egress device is enslaved to an L3 master device pass the
     * skb to its handler for processing
     */
    skb = l3mdev_ip_out(sk, skb);
    if (unlikely(!skb))
        return 0;
    skb->protocol = htons(ETH_P_IP);
    return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
               net, sk, skb, NULL, skb_dst(skb)->dev,
               dst_output);

返回的nf_hook函数,里面调用了dst_output,实质上是调用ip_finish__output函数

static int __ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    unsigned int mtu;
#if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM)
    /* Policy lookup after SNAT yielded a new policy */
    if (skb_dst(skb)->xfrm) {
        IPCB(skb)->flags |= IPSKB_REROUTED;
        return dst_output(net, sk, skb);
    }
#endif
    mtu = ip_skb_dst_mtu(sk, skb);
    if (skb_is_gso(skb))
        return ip_finish_output_gso(net, sk, skb, mtu);
    if (skb->len > mtu || (IPCB(skb)->flags & IPSKB_FRAG_PMTU))
        return ip_fragment(net, sk, skb, mtu, ip_finish_output2);
    return ip_finish_output2(net, sk, skb);

调用ip_fragment函数:

static int ip_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
               unsigned int mtu,
               int (*output)(struct net *, struct sock *, struct sk_buff *))
{
    struct iphdr *iph = ip_hdr(skb);
    if ((iph->frag_off & htons(IP_DF)) == 0)
        return ip_do_fragment(net, sk, skb, output);
    if (unlikely(!skb->ignore_df ||
             (IPCB(skb)->frag_max_size &&
              IPCB(skb)->frag_max_size > mtu))) {
        IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
        icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
              htonl(mtu));
        kfree_skb(skb);
        return -EMSGSIZE;
    }
    return ip_do_fragment(net, sk, skb, output);
}

最后调用dev_queue_xmit函数进行向下层发送包。

3.3.2 接收端

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 将会被发到本机还是会被转发还是丢弃:

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

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

调用的函数:

IP 层的入口函数在 ip_rcv 函数

/*
 * IP receive entry point
 */
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
       struct net_device *orig_dev)
{
    struct net *net = dev_net(dev);
    skb = ip_rcv_core(skb, net);
    if (skb == NULL)
        return NET_RX_DROP;
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
               net, NULL, skb, dev, NULL,
               ip_rcv_finish);

ip_rcv函数内部会调用ip_rcv_finish函数:

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;
}

dst_input函数会调用ip_local_deliver函数:

int ip_local_deliver(struct sk_buff *skb)
{
    /*
     *    Reassemble IP fragments.
     */
    struct net *net = dev_net(skb->dev);
    if (ip_is_fragment(ip_hdr(skb))) {
        if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
            return 0;
    }
    return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
               net, NULL, skb, skb->dev, NULL,
               ip_local_deliver_finish);
}

调用ip_protocol_deliver_rcu函数:

void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol)
{
    const struct net_protocol *ipprot;
    int raw, ret;
resubmit:
    raw = raw_local_deliver(skb, protocol);
    ipprot = rcu_dereference(inet_protos[protocol]);
    if (ipprot) {
        if (!ipprot->no_policy) {
            if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                kfree_skb(skb);
                return;
            }
            nf_reset_ct(skb);
        }
        ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv,
                      skb);
        if (ret < 0) {
            protocol = -ret;
            goto resubmit;
        }
        __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
    } else {
        if (!raw) {
            if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                __IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS);
                icmp_send(skb, ICMP_DEST_UNREACH,
                      ICMP_PROT_UNREACH, 0);
            }
            kfree_skb(skb);
        } else {
            __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
            consume_skb(skb);
        }
    }
}

3.4物理层与数据链路层

3.4.1 发送端

功能上,在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差错控制提供数据帧(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了。

3.4.2接收端

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。所以,驱动有两种方式通知内核:

通过以前的函数netif_rx;通过NAPI机制。该中断处理程序调用 Network device netif_rx_schedule 函数,进入软中断处理流程,再调用 net_rx_action 函数。

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

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

4. 时序图

 

 

 

5. 实验总结

次实验以LinuxTCP/IP协议栈的理论知识的基础深入分析Linux操作系统对于TCP/IP协议栈的实现原理与过程, 本次实验对LinuxTCP/IP协议有了更深的理解,使我受益匪浅。

 

posted @ 2021-01-30 18:04  周广维  阅读(114)  评论(0编辑  收藏  举报