TCP/IP协议栈在Linux内核中的运行时序分析
TCP/IP协议栈在Linux内核中的运行时序分析
0. 调研要求
- 在深入理解Linux内核任务调度(中断处理、softirg、tasklet、wq、内核线程等)机制的基础上,分析梳理send和recv过程中TCP/IP协议栈相关的运行任务实体及相互协作的时序分析。
- 编译、部署、运行、测评、原理、源代码分析、跟踪调试等
- 应该包括时序图
1. Linux系统概述
1.1 Linux系统架构
Linux系统一般由内核、Shell、文件系统和应用程序四大部分组成。其中,内核、Shell和文件系统一起形成了基本的操作系统架构,共同构建起应用程序的运行环境。

其中,内核是整个Linux系统的核心,也是Linux各项基本功能实现的基础。内核完成了Linux最核心、最基础的功能,包括进程调度、内存管理、设备驱动管理、文件系统和网络系统等等,决定着系统的性能和稳定性。
Linux的具体模块组成和系统架构可见下图。

1.2 Linux网络系统
1.2.1 OSI网络分层模型
开放式系统互联模型,简称为OSI模型。是一种概念模型,由国际标准化组织提出,一个试图使各种计算机在世界范围内互连为网络的标准框架。定义于ISO/IEC 7498-1。
开放式系统互联模型的开发始于上世纪70年代后期,用以支持各种计算机联网方法的出现。在上世纪80年代,该模型成为国际标准化组织(ISO)开放系统互连小组的工作产品。
OSI参考模型定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),如下图。每一层实现各自的功能和协议,并完成与相邻层的接口通信。OSI的服务定义详细说明了各层所提供的服务,某一层的服务就是该层及其下各层的一种能力,它通过接口提供给更高一层。各层所提供的服务与这些服务是怎么实现的无关。

TCP/IP协议栈在一定程度上参考了OSI参考模型的体系结构。OSI参考模型的七层结构在具体应用中无疑是有些复杂的,所以TCP/IP模型进行了一系列的简介:
- 应用层、表示层、会话层三个层次提供的服务相差不是很大,所以在TCP/IP协议中,它们被合并为应用层一个层次。
- 由于传输层和网络层在网络协议中的地位十分重要,所以在TCP/IP协议中它们被作为独立的两个层次。
- 因为数据链路层和物理层的内容相差不多,所以在TCP/IP协议中它们被归并在网络接口层一个层次里。
只有五层体系结构的TCP/IP协议,与有七层体系结构的OSI相比要简单了不少,也正是这样,TCP/IP协议在实际的应用中效率更高,成本更低。
TCP/IP网络模型与OSI参考模型的对应关系,以及工作在对应层的网络设备: 
1.2.2 Linux内核中的网络协议栈
linux网络栈的层次结构非常清晰,并没有按照OSI七层模型来实现,而是压缩并扩展了一些层。从上而下,依次为应用层,系统调用接口层,协议无关接口层,网络栈层,设备无关接口层,设备驱动层。因为linux的网络栈中的socket是继承自BSD的,socket插口为应用层使用网络服务提供了简单的方法,它屏蔽了具体的下层协议族的差异。下面重点说一下中间的4层。
- 系统调用接口层。系统调用接口层提供了socket接口的系统调用。
- 协议无关接口层。网络世界里是有很有种协议族的,比如我们最常用的TCP/IPv4协议族,但是除此之外还有很多协议族存在,比如netlink,unix等,因此,为了使用上的方便,抽象了一个协议无关接口层,只需要在创建socket时,传入对应的参数,就能创建出对应的协议族socket类型。具体的可以看一下socket函数的参数:socket(int domain, int type, int protocol);第一个参数就定义了使用的协议族。第二个参数就是指定socket类型。一般来说,前两个参数就能确定一个socket的类型和使用的传输层协议了,如流式套接字对应使用TCP/IP中的TCP协议,用户数据包对应使用TCP/IP中的UDP协议。
- 网络栈层。这一层就是具体的各类协议的实现了。包括传输层和网络层。对于我们最经常使用的tcp/ip来说,传输层主要包括TCP和UDP协议,网络层就是IP协议。
- 设备无关接口层。这一层夹在网络栈和驱动层之间,网络设备种类多样,当收到数据包时,如果没有设备无关接口层的抽象,势必会导致两层之间的调用复杂,因此,有必要抽象出设备无关层,如驱动向上的传递接口,通用设备表示等。从这个设计来看,给我们很多启示,联想上面的协议无关接口层,可以看出,在一对多这种情况下,设计一个通用层会有很多好处。

1.2.3 Linux网络协议栈文件分布
- 文件的实现主要在/linux/net目录下。
- /linux/net目录下的几乎每个文件夹就是一个协议族的集合。如IPv4,IPv6,802,ATM。
- 对于ipv4的网络层,传输层的实现分布在/linux/net/ipv4中。
- BSD Socket层主要文件有/net/socket.c、/net/protocol.c等。
- INET socket层主要文件有/net/ipv4/protocol.c、/net/ipv4/af_inet.c、/net/core/sock.c等。
- 网络层文件主要有/net/ipv4/ip_forward.c、ip_fragment.c、ip_input.c等。
- 协议无关接口层和设备无关接口层分布在/linux/net/core文件夹中。
1.2.4 协议栈涉及的主要函数和数据结构
2. send和recv在应用层运行时序
2.1 网络通信程序源码
本次调研,我们通过追踪一个基于TCP协议的Socket通信程序,实现对整个TCP/IP协议栈在Linux内核中运行时序的分析。
Socket通信程序完成的功能非常简单——客户端与服务端建立TCP连接,接着由客户端向服务端发送send()一个“Hi”消息,待服务器端接收recv()到客户端发来的“Hi”消息后,向客户端回应send()一个“Hello”消息,客户端收到recv()回应后,关闭通信,结束程序。
在程序运行过程中,我们需要关注send()和recv()的具体执行流程。send()和recv()是Socket提供的数据收发API,通过调用send()和recv(),网络通信传输的数据经历应用层、传输层、网络层、数据链路层一直到网卡转换成电信号传送到接收方主机,再经历与之前相反的流程回到接收方主机的应用层,完成一次网络程序之间的通信。
2.1.1 服务端代码
#include <stdio.h> /* perror */
#include <stdlib.h> /* exit */
#include <sys/types.h> /* WNOHANG */
#include <sys/wait.h> /* waitpid */
#include <string.h> /* memset */
#define true 1
#define false 0
#define PORT 3490 /* 监听的端口 */
#define MAXSIZE_OF_LISTEN 10 /* listen的请求接收队列长度 */
#define BUFFER_SIZE 255
int main()
{
int serverSocketFd = socket(AF_INET, SOCK_STREAM, 0); //protocol为0,自动选择type对应的默认协议
if (serverSocketFd < 0)
{
perror("Create Socket Failed: ");
exit(1);
}
struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT); /* 网络字节顺序调整 */
serverAddr.sin_addr.s_addr = htons(INADDR_ANY); /* 自动填入本机IP地址 */
/* bind绑定socket和Server Address */
if (bind(serverSocketFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1)
{
perror("Server Bind Failed: ");
exit(1);
}
/* 调用listen,让socket等待接收client的连接 */
if (listen(serverSocketFd, MAXSIZE_OF_LISTEN) == -1)
{
perror("Server Listen Failed: ");
exit(1);
}
printf("Server socket init successfully. Then be ready for listen...\n");
while (1)
{
char buffer[BUFFER_SIZE];
struct sockaddr_in clientAddr;
socklen_t socketInLen = sizeof(struct sockaddr_in);
int clientSocketFd = accept(serverSocketFd, (struct sockaddr *)&clientAddr, &socketInLen);
if (clientSocketFd < 0)
{
perror("Server Accept Failed: ");
}
memset(buffer, 0, BUFFER_SIZE);
if (recv(clientSocketFd, buffer, BUFFER_SIZE, 0) < 0)
{
perror("Server Receive Message Failed: ");
}
if (strcmp(buffer, "hi")==0 || strcmp(buffer, "Hi")==0 || strcmp(buffer, "Hello")==0 || strcmp(buffer, "hello")==0)
{
if (fork() == 0)
{
if (send(clientSocketFd, "Hello\n", 6, 0) < 0)
{
printf("Send Failed./n");
}
exit(0);
}
}
close(clientSocketFd);
/*清除所有子进程 */
while (Waitpid(-1, NULL, WNOHANG) > 0);
}
}
2.1.2 客户端代码
#include <stdio.h> /* perror */
#include <stdlib.h> /* exit */
#include <sys/types.h> /* WNOHANG */
#include <sys/wait.h> /* waitpid */
#include <string.h> /* memset */
#include "socketwrapper.h"
#define true 1
#define false 0
#define PORT 3490 /* Server的端口 */
#define MAXDATASIZE 100 /* 一次可以读的最大字节数 */
#define BUFFER_SIZE 255
int main()
{
struct sockaddr_in clientAddr;
memset(&clientAddr, 0, sizeof(clientAddr));
clientAddr.sin_family = AF_INET;
clientAddr.sin_addr.s_addr = htons(INADDR_ANY);
clientAddr.sin_port = htons(10080);
int clientSocketFd = socket(AF_INET, SOCK_STREAM, 0);
if (clientSocketFd < 0)
{
perror("Create Socket Failed: ");
exit(1);
}
if (bind(clientSocketFd, (struct sockaddr *)&clientAddr, sizeof(clientAddr)) == -1)
{
perror("Client Bind Failed: ");
exit(1);
}
struct sockaddr_in serverAddr; /* 对方地址信息 */
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = Htons(PORT); /* short, NBO */
// 将点分十进制串转换成网络字节序二进制值
if (inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr) == 0)
{
perror("Server IP Address Error:");
exit(1);
}
socklen_t serverAddrLen = sizeof(serverAddr);
if (connect(clientSocketFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1)
{
perror("Can't Connect To Server: ");
exit(1);
}
char buffer[BUFFER_SIZE];
memset(buffer, 0, BUFFER_SIZE);
printf("Client: ");
scanf("%s", buffer);
if (send(clientSocketFd, buffer, strlen(buffer), 0) < 0)
{
perror("Send Failed:");
}
memset(buffer, 0, BUFFER_SIZE);
if (recv(clientSocketFd, buffer, BUFFER_SIZE, 0) < 0)
{
perror("Server Receive Message Failed: ");
}
printf("Server: %s\n", buffer);
close(clientSocketFd);
/*清除所有子进程 */
while (Waitpid(-1, NULL, WNOHANG) > 0);
return true;
}
2.2 Socket套接字
2.2.1 什么是Socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
Socket是一个接口,在用户进程与TCP/IP协议之间充当中间人,完成TCP/IP协议的书写,用户只需理解接口即可。

Socket起源于Unix,而Unix/Linux 基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式 来操作。Socket就是该模式的一个实现,Socket即是一种特殊的文件,一些Socket函数就是对其进行的操作(读/写IO、打开、关闭)。
常用的套接字是伯克利套接字,也称为BSD Socket。伯克利套接字的应用编程接口(API)是采用C语言的进程间通信的库,经常用在计算机网络间的通信。 BSD Socket的应用编程接口已经是网络套接字的抽象标准。大多数其他程序语言使用一种相似的编程接口。它最初是由加州伯克利大学为Unix系统开发出来的。所有现代的操作系统都实现了伯克利套接字接口,因为它已经是连接互联网的标准接口了。
2.2.2 socket的创建——socket
在用户进程中,socket(int domain, int type, int protocol) 函数用于创建socket并返回一个与socket关联的fd,该函数经过 glibc 库对其封装,它将通过int 0x80产生一个软件中断(注意不是软中断),实际执行的是系统调用 sys_socketcall,sys_socketcall几乎是用户进程socket所有操作函数的入口。
int serverSocketFd = socket(AF_INET, SOCK_STREAM, 0); //protocol为0,自动选择type对应的默认协议`
代码执行流程:
net/Socket.c:sys_socketcall(),根据系统调用号,创建socket会执行sys_socket()函数;- 分配socket结构,依次调用
net/Socket.c:sys_socket()->sock_create()->__sock_create()->sock_alloc(),在socket文件系统中创建i节点inode = new_inode(sock_mnt->mnt_sb); - 创建socket专用inode,分配inode会调用sock_alloc_inode函数来完成,实际上分配了一个socket_alloc结构体,该结构体包含socket和inode,但最终返回的是该结构体中的inode成员。至此,socket结构和inode结构均分配完毕;
- 根据inode取得socket对象;
- 使用协议族来初始化socket:
- 注册AF_INET协议域。初始化入口
net/ipv4/Af_inet.c:这里调用sock_register()函数来完成注册,根据family将AF_INET协议域inet_family_ops注册到内核中的net_families数组中。 - 套接字类型。在相同的协议域下,可能会存在多个套接字类型;如AF_INET域下存在流套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW),在这三种类型的套接字上建立的协议分别是TCP, UDP,ICMP/IGMP等。AF_IENT域的这三种套接字类型定义用结构体
inet_protosw(net/ipv4/Af_inet.c)来表示。 - 使用协议域来初始化socket。
- 注册AF_INET协议域。初始化入口
- 分配sock结构以及建立socket结构与sock结构的关系;
- 使用tcp协议初始化sock。
inet_create()函数最后,调用的是tcp_prot的init钩子函数net/ipv4/Tcp_ipv4.c:tcp_v4_init_sock(),主要是对tcp_sock和inet_connection_sock进行一些初始化; - socket与文件系统关联。创建好与socket相关的结构后,需要与文件系统关联,具体在sock_map_fd()函数中完成。socket与文件系统关联后,以后便可以通过文件系统read/write对socket进行操作了。
socket创建过程的调用链如下图:
socket创建过程中的数据结构如图:
调试验证:
2.2.3 socket发送数据——send
在创建完 socket 之后, 不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。
由于socket所有操作都是通过系统调用sys_socketcall()进入内核态的,所以首先要确定send()操作的内核入口程序。

可以发现,send()和sendto()调用的都是__sys_sendto(),只是参数上有些许的不同。
继续跟踪,分析__sys_sendto()函数,该函数的作用是:1.通过fd获取了对应的socket;2.创建了用来描述待发送数据的结构体struct msghdr;3.调用了sock_sendmsg执行数据的发送。
下面分析源码:
/**
* fd:待发送数据的socket文件描述符
* buff: 待发送数据的缓冲区指针
* len:待发送数据的长度
* flags:数据发送的标志
* addr:目的地址
* addr_len:目的地址长度
*/
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;
//通过文件描述符查找socket信息
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;
}
//如果采取了非阻塞方式,那么还需要设置flags
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
msg.msg_flags = flags;
//调用sock_sendmsg发送数据
err = sock_sendmsg(sock, &msg);
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
}
通过分析__sys_sendto()源码,发现下一步通过sock_sendmsg()进行数据发送,所以继续分析sock_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);
}
继续追踪sock_sendmsg_nosec():
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
int ret = INDIRECT_CALL_INET(sock->ops->sendmsg, inet6_sendmsg,
inet_sendmsg, sock, msg,
msg_data_left(msg));
BUG_ON(ret == -EIOCBQUEUED);
return ret;
}
继续追踪,发现最终调用了inet_sendmsg():
int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
struct sock *sk = sock->sk;
if (unlikely(inet_send_prepare(sk)))
return -EAGAIN;
return INDIRECT_CALL_2(sk->sk_prot->sendmsg, tcp_sendmsg, udp_sendmsg, sk, msg, size);
}
在inet_sendmsg()函数的最后,调用了tcp_sendmsg(),程序离开应用层,进入传输层。
总结一下,send()在应用层中的调用链应该是这样的:
调试验证:
2.2.4 socket接收数据——recv
recv()函数与send()函数类似,调用recv()后,通过系统调用sys_socketcall()进入内核态,在源码中可以找到recv()对应的系统调用的入口代码:

可以看到,与send()类似,recv()和recvfrom()也都是调用的__sys_recvfrom(),只是在传入的参数上有所不同,recv()可以看做是recvfrom()一种特殊情况的封装函数。
沿着调用路径一直追踪,可以明显的发现,recv()的调用过程和send()非常相似。
/**
* fd:要接收数据的socket文件描述符
* ubuf: 数据接收缓冲区指针
* size:接收缓冲区的大小
* flags:数据接收的标志
* addr:源地址
* addr_len:源地址长度
*/
int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
struct sockaddr __user *addr, int __user *addr_len)
{
struct socket *sock;
struct iovec iov;
struct msghdr msg;
struct sockaddr_storage address;
int err, err2;
int fput_needed;
err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
//根据fd找到对应的socket
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
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;
//调用sock_recvmsg()进行发送
err = sock_recvmsg(sock, &msg, flags);
if (err >= 0 && addr != NULL) {
err2 = move_addr_to_user(&address,
msg.msg_namelen, addr, addr_len);
if (err2 < 0)
err = err2;
}
fput_light(sock->file, fput_needed);
out:
return err;
}
追踪sock_recvmsg():

继续看sock_recvmsg_nosec():

间接调用了inet_recvmsg():

在这里调用tcp_recvmsg()进入传输层。
总结一下,recv()在应用层中的调用链应该是这样的:
调试验证:
3. send和recv在传输层的运行时序
3.1 TCP的数据发送
从应用层的分析中,我们可以得知,send()函数最终调用了传输层的tcp_sendmsg()函数,从这里开始,数据发送进入了传输层。下面我们分析的就是TCP的数据发送过程:
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
int ret;
lock_sock(sk);
ret = tcp_sendmsg_locked(sk, msg, size);
release_sock(sk);
return ret;
}
可以发现,tcp_sendmsg()只是做了一个上锁的操作,设置了一个临界区,实际的数据发送调用了tcp_sendmsg_locked()函数。
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
struct tcp_sock *tp = tcp_sk(sk);/*进行了强制类型转换*/
struct ubuf_info *uarg = NULL;
struct sk_buff *skb;
struct sockcm_cookie sockc;
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,里面存放的是待发送的数据。
struct sock{
...
struct sk_buff_head sk_write_queue;/*指向skb队列的第一个元素*/
...
struct sk_buff *sk_send_head;/*指向队列第一个还没有发送的元素*/
}
之后,调用了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协议的头部有几个标志字段:URG、ACK、RSH、RST、SYN、FIN,tcp_push()中会判断这个skb的元素是否需要push,如果需要就将tcp头部字段的push置一。
整个过程会有些复杂,首先struct tcp_skb_cb结构体存放的就是tcp的头部,头部的控制位为tcp_flags,通过tcp_mark_push()会将skb中的cb,也就是48个字节的数组,类型转换为struct tcp_skb_cb,这样位于skb的cb就成了tcp的头部。
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);
......
}
tcp_transmit_skb()是tcp发送数据位于传输层的最后一步,这里首先对TCP数据段的头部进行了处理,然后调用了网络层提供的发送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl)实现了数据的发送,自此,数据离开了传输层,传输层的任务也就结束了。
TCP在传输层的数据发送调用链如下图:
调试验证:
3.2 TCP的数据接收
tcp_recvmsg()接收函数要比tcp_sendmsg()发送函数要复杂得多,因为数据接收不仅仅只是接收,tcp的三次握手也是在接收函数实现的,所以收到数据后要判断当前的状态,是否正在建立连接等,根据发来的信息考虑状态是否要改变,为了专注调研课题,在这里仅仅考虑在连接建立后数据的接收。
首先分析上一层的调用的tcp_recvmsg()。该函数完成从接收队列中读取数据复制到用户空间的任务;函数在执行过程中会锁定控制块,避免软中断在 tcp 层的影响;函数会涉及从接收队列recive_que和后备队列backlog中读取数据;其中从 backlog 中读取的数据,还需要经过sk_backlog_rcv()回调,该回调的实现为 tcp_v4_do_rcv(),实际上是先缓存到队列中,然后需要读取的时候,才进入协议栈处理,此时,是在进程上下文执行的,因为会设置 tp->ucopy.task=curent,在协议栈处理过程中,会直接将数据复制到用户空间。
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);
······
}
这里共维护了三个队列:prequeue、backlog、receive_queue,分别为预处理队列,后备队列和接收队列。
在连接建立后,若没有数据到来,接收队列为空,进程会在sk_busy_loop()函数内循环等待,直到接收队列不为空。数据到来后,调用函数skb_copy_datagram_msg()将接收到的数据拷贝到用户态,实际调用的是__skb_datagram_iter(),这里同样用了struct msghdr *msg来实现。
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;
}
如果目标数据读取完,则处理后备队列。但是如果没有设置noblock,同时也没有出现copied >= target的情况,也就是没有读到足够多的数据,则调用 sk_wait_data()将当前进程等待。也就是我们希望的阻塞方式。阻塞函数sk_wait_data() 所做的事情就是让出 CPU,等数据来了或者设定超时之后再恢复运行。
从下向上分析,即 tcp 层是如何接收来自 ip 的数据并且插入相应队列的。tcp_v4_rcv()函数为 TCP 的总入口,数据包从 IP 层传递上来,进入该函数。其协议操作函数结构如下所示,其中handler即为 IP 层向 TCP 传递数据包的回调函数,设置为tcp_v4_rcv()。

在 IP 层处理本地数据包时,会获取到上述结构的实例,并且调用实例的 handler 回调,也就是调用了tcp_v4_rcv()。
tcp_v4_rcv ()函数只要做以下几个工作:
- 设置 TCP_CB
- 查找控制块
- 根据控制块状态做不同处理,包括 TCP_TIME_WAIT 状态处理,TCP_NEW_SYN_RECV 状态处理,TCP_LISTEN 状态处理
- 接收 TCP 段。


可以看到具体过程是检测连接状态最后调用具体的接收处理函数tcp_v4_do_rcv():
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
struct dst_entry *dst = sk->sk_rx_dst;
sock_rps_save_rxhash(sk, skb);
sk_mark_napi_id(sk, skb);
if (dst) {
if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif ||
!dst->ops->check(dst, 0)) {
dst_release(dst);
sk->sk_rx_dst = NULL;
}
}
tcp_rcv_established(sk, skb);
return 0;
}
······
if (tcp_rcv_state_process(sk, skb)) {
rsk = sk;
goto reset;
}
return 0;
reset:
tcp_v4_send_reset(rsk, skb);
······
}
建立连接之后利用tcp_rcv_established来进行数据的接收:
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
const struct tcphdr *th = (const struct tcphdr *)skb->data;
struct tcp_sock *tp = tcp_sk(sk);
unsigned int len = skb->len;
······
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
int tcp_header_len = tp->tcp_header_len;
······
} else {
int eaten = 0;
bool fragstolen = false;
if (tcp_checksum_complete(skb))
goto csum_error;
if ((int)skb->truesize > sk->sk_forward_alloc)
goto step5;
if (tcp_header_len == (sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) && tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp);
tcp_rcv_rtt_measure_ts(sk, skb);
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPHPHITS);
/* Bulk data transfer: receiver */
__skb_pull(skb, tcp_header_len);
eaten = tcp_queue_rcv(sk, skb, &fragstolen);
tcp_event_data_recv(sk, skb);
if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
/* Well, only one small jumplet in fast path... */
tcp_ack(sk, skb, FLAG_DATA);
tcp_data_snd_check(sk);
if (!inet_csk_ack_scheduled(sk))
goto no_ack;
}
__tcp_ack_snd_check(sk, 0);
no_ack:
if (eaten)
kfree_skb_partial(skb, fragstolen);
tcp_data_ready(sk);
return;
}
}
······
}
在tcp_rcv_established()这个函数中,涉及到的逻辑比较复杂,涉及到一系列的标志位检查,状态处理的过程,当然这也是tcp协议必须保证的一个特征。到最后返回值里面,有一个tcp_queue_rcv()函数,查看这个函数:
static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb, bool *fragstolen)
{
int eaten;
struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);
eaten = (tail && tcp_try_coalesce(sk, tail, skb, fragstolen)) ? 1 : 0;
tcp_rcv_nxt_update(tcp_sk(sk), TCP_SKB_CB(skb)->end_seq);
if (!eaten) {
__skb_queue_tail(&sk->sk_receive_queue, skb);
skb_set_owner_r(skb, sk);
}
return eaten;
}
struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue)这个语句表明将发送的消息添加到队列的最尾端,即相当于发送之后进行系统调用唤醒socket(一切正常的情况下),然后再利用应用层提到的tcp_recvmsg()函数去进行消息的处理。
TCP在传输层的数据接收调用链如下图:
调试验证:
4. send和recv在网络层的运行时序
4.1 IP协议的数据发送
网络层IP的入口函数是ip_queue_xmit(),ip_queue_xmit()是 ip 层提供给 tcp 层发送回调函数。ip_queue_xmit()完成面向连接套接字的包输出,当套接字处于连接状态时,所有从套接字发出的包都具有确定的路由,无需为每一个输出包查询它的目的入口,可将套接字直接绑定到路由入口上。
ip_queue_xmit()首先为输入包建立IP包头, 经过本地包过滤器后,再将IP包分片输出ip_fragment()。
static inline int ip_queue_xmit(struct sock *sk, struct sk_buff *skb,struct flowi *fl)
{
return __ip_queue_xmit(sk, skb, fl, inet_sk(sk)->tos);
}
ip_que_xmit() 实际上是调用__ip_que_xmit()。
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;
rcu_read_lock();
inet_opt = rcu_dereference(inet->inet_opt);
fl4 = &fl->u.ip4;
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;
/* Use correct destination address if we have options. */
daddr = inet->inet_daddr;
if (inet_opt && inet_opt->opt.srr)
daddr = inet_opt->opt.faddr;
/* If this fails, retransmit mechanism of transport layer will
* keep trying until route appears or the connection times
* itself out.
*/
rt = ip_route_output_ports(net, fl4, sk,
daddr, inet->inet_saddr,
inet->inet_dport,
inet->inet_sport,
sk->sk_protocol,
RT_CONN_FLAGS_TOS(sk, tos),
sk->sk_bound_dev_if);
if (IS_ERR(rt))
goto no_route;
sk_setup_caps(sk, &rt->dst);
}
skb_dst_set_noref(skb, &rt->dst);
packet_routed:
if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway)
goto no_route;
/* OK, we know where to send it, allocate and build IP header. */
skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
skb_reset_network_header(skb);
iph = ip_hdr(skb);
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (tos & 0xff));
if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
iph->ttl = ip_select_ttl(inet, &rt->dst);
iph->protocol = sk->sk_protocol;
ip_copy_addrs(iph, fl4);
/* Transport layer set skb->h.foo itself. */
if (inet_opt && inet_opt->opt.optlen) {
iph->ihl += inet_opt->opt.optlen >> 2;
ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0);
}
ip_select_ident_segs(net, skb, sk,
skb_shinfo(skb)->gso_segs ?: 1);
/* TODO : should we use skb->sk here instead of sk ? */
skb->priority = sk->sk_priority;
skb->mark = sk->sk_mark;
res = ip_local_out(net, sk, skb);
rcu_read_unlock();
return res;
no_route:
rcu_read_unlock();
IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
kfree_skb(skb);
return -EHOSTUNREACH;
}
EXPORT_SYMBOL(__ip_queue_xmit);
__ip_queue_xmit()会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由。
紧接着根据代码可知,会进行分片和字段填充等工作,如果数据包大于最大长度mtu,则进行分片,否则直接发出去,调用的函数是ip_finish_output(),进而调用__ip_finish_output()。
static int ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
int ret;
ret = BPF_CGROUP_RUN_PROG_INET_EGRESS(sk, skb);
switch (ret) {
case NET_XMIT_SUCCESS:
return __ip_finish_output(net, sk, skb);
case NET_XMIT_CN:
return __ip_finish_output(net, sk, skb) ? : ret;
default:
kfree_skb(skb);
return ret;
}
}
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_finish_output2()发送数据报,在转发的过程中,neigh_output(),neigh_hh_output()(缓存)被调用,选择具体的路由进行转发,最终调用dev_queue_xmit()将数据包考本到链路层skb,交由下一层处理。相关代码如下:
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct dst_entry *dst = skb_dst(skb);
struct rtable *rt = (struct rtable *)dst;
struct net_device *dev = dst->dev;
unsigned int hh_len = LL_RESERVED_SPACE(dev);
struct neighbour *neigh;
bool is_v6gw = false;
if (rt->rt_type == RTN_MULTICAST) {
IP_UPD_PO_STATS(net, IPSTATS_MIB_OUTMCAST, skb->len);
} else if (rt->rt_type == RTN_BROADCAST)
IP_UPD_PO_STATS(net, IPSTATS_MIB_OUTBCAST, skb->len);
/* Be paranoid, rather than too clever. */
if (unlikely(skb_headroom(skb) < hh_len && dev->header_ops)) {
struct sk_buff *skb2;
skb2 = skb_realloc_headroom(skb, LL_RESERVED_SPACE(dev));
if (!skb2) {
kfree_skb(skb);
return -ENOMEM;
}
if (skb->sk)
skb_set_owner_w(skb2, skb->sk);
consume_skb(skb);
skb = skb2;
}
if (lwtunnel_xmit_redirect(dst->lwtstate)) {
int res = lwtunnel_xmit(skb);
if (res < 0 || res == LWTUNNEL_XMIT_DONE)
return res;
}
rcu_read_lock_bh();
neigh = ip_neigh_for_gw(rt, skb, &is_v6gw);
if (!IS_ERR(neigh)) {
int res;
sock_confirm_neigh(skb, neigh);
/* if crossing protocols, can not use the cached header */
res = neigh_output(neigh, skb, is_v6gw);
rcu_read_unlock_bh();
return res;
}
rcu_read_unlock_bh();
net_dbg_ratelimited("%s: No header cache and no neighbour!\n",
__func__);
kfree_skb(skb);
return -EINVAL;
}
在构造好 ip 头,检查完分片之后,会调用邻居子系统的输出函数 neigh_output()进行输出:
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb, bool skip_cache)
{
const struct hh_cache *hh = &n->hh;
if ((n->nud_state & NUD_CONNECTED) && hh->hh_len && !skip_cache)
return neigh_hh_output(hh, skb);
else
return n->output(n, skb);
}
输出分为有二层头缓存和没有两种情况,有缓存时调用neigh_hh_output()进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出:
static inline int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb)
{
unsigned int hh_alen = 0;
unsigned int seq;
unsigned int hh_len;
do {
seq = read_seqbegin(&hh->hh_lock);
hh_len = READ_ONCE(hh->hh_len);
if (likely(hh_len <= HH_DATA_MOD)) {
hh_alen = HH_DATA_MOD;
/* skb_push() would proceed silently if we have room for
* the unaligned size but not for the aligned size:
* check headroom explicitly.
*/
if (likely(skb_headroom(skb) >= HH_DATA_MOD)) {
/* this is inlined by gcc */
memcpy(skb->data - HH_DATA_MOD, hh->hh_data,
HH_DATA_MOD);
}
} else {
hh_alen = HH_DATA_ALIGN(hh_len);
if (likely(skb_headroom(skb) >= hh_alen)) {
memcpy(skb->data - hh_alen, hh->hh_data,
hh_alen);
}
}
} while (read_seqretry(&hh->hh_lock, seq));
if (WARN_ON_ONCE(skb_headroom(skb) < hh_alen)) {
kfree_skb(skb);
return NET_XMIT_DROP;
}
__skb_push(skb, hh_len);
return dev_queue_xmit(skb);
}
最后调用dev_queue_xmit()函数进行向链路层发送包。
IP层的数据发送调用链如下图:
调试验证:
4.2 IP协议的数据接收
IP 层的入口函数在ip_rcv()函数:
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_finish()这个函数接口,如果是发到本机就调用dst_input():
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;
}
static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)->input(skb);
}
根据源码可以看出发向上层的数据时调用ip_local_deliver()函数,可能会合并IP包,然后调用ip_local_deliver()函数。该函数根据 package 的下一个处理层的protocal number,调用下一层接口,包括 tcp_v4_rcv()等,对于 TCP 来说,函数tcp_v4_rcv()函数会被调用,从而处理流程进入 TCP 栈。由此可以和我们刚刚追踪的传输层的函数连接起来;当然,更新路由的时候如果是转发而不是发送到本机则向下层处理:
int ip_local_deliver(struct sk_buff *skb)
{
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_defrag()进行合并多个数据包的操作:
int ip_defrag(struct net *net, struct sk_buff *skb, u32 user)
{
struct net_device *dev = skb->dev ? : skb_dst(skb)->dev;
int vif = l3mdev_master_ifindex_rcu(dev);
struct ipq *qp;
__IP_INC_STATS(net, IPSTATS_MIB_REASMREQDS);
skb_orphan(skb);
/* Lookup (or create) queue header */
qp = ip_find(net, ip_hdr(skb), user, vif);
if (qp) {
int ret;
spin_lock(&qp->q.lock);
ret = ip_frag_queue(qp, skb);
spin_unlock(&qp->q.lock);
ipq_put(qp);
return ret;
}
__IP_INC_STATS(net, IPSTATS_MIB_REASMFAILS);
kfree_skb(skb);
return -ENOMEM;
}
没有分片就调用ip_local_deliver_finish():
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
__skb_pull(skb, skb_network_header_len(skb));
rcu_read_lock();
ip_protocol_deliver_rcu(net, skb, ip_hdr(skb)->protocol);
rcu_read_unlock();
return 0;
}
进一步调用ip_protocol_deliver_rcu(),该函数根据 package 的下一个处理层的protocal number,调用下一层接口,包括tcp_v4_rcv()(TCP),udp_rcv ()(UDP)。对于 TCP 来说,函数tcp_v4_rcv()函数会被调用,从而处理流程进入 TCP 栈:
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);
}
}
}
IP协议数据接收的调用链如下图:
调试验证:

5.send和recv在数据链路层和物理层的运行时序
5.1 数据发送
网络层调用dev_queue_xmit()进入链路层的处理流程,实际上调用的是__dev_queueue_xmit()。
int dev_queue_xmit(struct sk_buff *skb)
{
return __dev_queue_xmit(skb, NULL);
}
static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{
struct net_device *dev = skb->dev;
struct netdev_queue *txq;
struct Qdisc *q;
int rc = -ENOMEM;
bool again = false;
skb_reset_mac_header(skb);
if (unlikely(skb_shinfo(skb)->tx_flags & SKBTX_SCHED_TSTAMP))
__skb_tstamp_tx(skb, NULL, skb->sk, SCM_TSTAMP_SCHED);
/* Disable soft irqs for various locks below. Also
* stops preemption for RCU.
*/
rcu_read_lock_bh();
skb_update_prio(skb);
qdisc_pkt_len_init(skb);
#ifdef CONFIG_NET_CLS_ACT
skb->tc_at_ingress = 0;
# ifdef CONFIG_NET_EGRESS
if (static_branch_unlikely(&egress_needed_key)) {
skb = sch_handle_egress(skb, &rc, dev);
if (!skb)
goto out;
}
# endif
#endif
if (dev->priv_flags & IFF_XMIT_DST_RELEASE)
skb_dst_drop(skb);
else
skb_dst_force(skb);
txq = netdev_core_pick_tx(dev, skb, sb_dev);
q = rcu_dereference_bh(txq->qdisc);
trace_net_dev_queue(skb);
if (q->enqueue) {
rc = __dev_xmit_skb(skb, q, dev, txq);
goto out;
}
if (dev->flags & IFF_UP) {
int cpu = smp_processor_id(); /* ok because BHs are off */
if (txq->xmit_lock_owner != cpu) {
if (dev_xmit_recursion())
goto recursion_alert;
skb = validate_xmit_skb(skb, dev, &again);
if (!skb)
goto out;
HARD_TX_LOCK(dev, txq, cpu);
if (!netif_xmit_stopped(txq)) {
dev_xmit_recursion_inc();
skb = dev_hard_start_xmit(skb, dev, txq, &rc);
dev_xmit_recursion_dec();
if (dev_xmit_complete(rc)) {
HARD_TX_UNLOCK(dev, txq);
goto out;
}
}
HARD_TX_UNLOCK(dev, txq);
net_crit_ratelimited("Virtual device %s asks to queue packet!\n",
dev->name);
} else {
/* Recursion is detected! It is possible,
* unfortunately
*/
recursion_alert:
net_crit_ratelimited("Dead loop on virtual device %s, fix it urgently!\n",
dev->name);
}
}
rc = -ENETDOWN;
rcu_read_unlock_bh();
atomic_long_inc(&dev->tx_dropped);
kfree_skb_list(skb);
return rc;
out:
rcu_read_unlock_bh();
return rc;
}
__dev_queue_xmit会调用dev_hard_start_xmit函数获取skb:
struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev, struct netdev_queue *txq, int *ret)
{
struct sk_buff *skb = first;
int rc = NETDEV_TX_OK;
while (skb) {
struct sk_buff *next = skb->next;
skb_mark_not_on_list(skb);
rc = xmit_one(skb, dev, txq, next != NULL);
if (unlikely(!dev_xmit_complete(rc))) {
skb->next = next;
goto out;
}
skb = next;
if (netif_tx_queue_stopped(txq) && skb) {
rc = NETDEV_TX_BUSY;
break;
}
}
out:
*ret = rc;
return skb;
}
最终的数据通过xmit_one()这个函数传递给物理层的设备,到这里虚拟的传递的驱动就要结束了,将和实际的设备驱动连接起来:
static int xmit_one(struct sk_buff *skb, struct net_device *dev, struct netdev_queue *txq, bool more)
{
unsigned int len;
int rc;
if (dev_nit_active(dev))
dev_queue_xmit_nit(skb, dev);
len = skb->len;
trace_net_dev_start_xmit(skb, dev);
rc = netdev_start_xmit(skb, dev, txq, more);
trace_net_dev_xmit(skb, rc, dev, len);
return rc;
}
调用netdev_start_xmit(),实际上是调用__netdev_start_xmit()。
物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中,同时在数据的拷贝中,还会加入相关协议等。对于以太网网络,物理层发送采用CSMA/CD协议,即在发送过程中侦听链路冲突。一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。到这一步,这个数据就可以完整的输出到物理层设备上了,转化为比特流的形式。
static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops, struct sk_buff *skb, struct net_device *dev, bool more)
{
__this_cpu_write(softnet_data.xmit.more, more);
return ops->ndo_start_xmit(skb, dev);
}
调用各网络设备实现的ndo_start_xmit()回调函数指针,其为数据结构struct net_device,从而把数据发送给网卡,物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中。在数据拷贝中,同时加入符合以太网协议的相关header,IFG、前导符和CRC。对于以太网网络,物理层发送采用CSMA/CD,即在发送过程中侦听链路冲突。
一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。
数据链路层和物理层的数据发送调用链如下图:
调试验证:
5.2 数据接收
-
包到达机器的物理网卡时候触发一个中断,并将通过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())。
在linux5.4.34内核中,利用一组特殊的API 来处理接收的数据帧,即NAPI,通过NAPI机制该中断处理程序调用Network device的netif_rx_schedule()函数,进入软中断处理流程,再调用net_rx_action()函数:
static __latent_entropy void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
unsigned long time_limit = jiffies +
usecs_to_jiffies(netdev_budget_usecs);
int budget = netdev_budget;
LIST_HEAD(list);
LIST_HEAD(repoll);
local_irq_disable();
list_splice_init(&sd->poll_list, &list);
local_irq_enable();
for (;;) {
struct napi_struct *n;
if (list_empty(&list)) {
if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))
goto out;
break;
}
n = list_first_entry(&list, struct napi_struct, poll_list);
budget -= napi_poll(n, &repoll);
/* If softirq window is exhausted then punt.
* Allow this to run for 2 jiffies since which will allow
* an average latency of 1.5/HZ.
*/
if (unlikely(budget <= 0 ||
time_after_eq(jiffies, time_limit))) {
sd->time_squeeze++;
break;
}
}
local_irq_disable();
list_splice_tail_init(&sd->poll_list, &list);
list_splice_tail(&repoll, &list);
list_splice(&list, &sd->poll_list);
if (!list_empty(&sd->poll_list))
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
net_rps_action_and_irq_enable(sd);
out:
__kfree_skb_flush();
}
net_rx_action()调用网卡驱动里的napi_poll()函数来一个一个的处理数据包。在napi_poll()函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道。驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用napi_gro_receive()函数:
static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{
void *have;
int work, weight;
list_del_init(&n->poll_list);
have = netpoll_poll_lock(n);
weight = n->weight;
/* This NAPI_STATE_SCHED test is for avoiding a race
* with netpoll's poll_napi(). Only the entity which
* obtains the lock and sees NAPI_STATE_SCHED set will
* actually make the ->poll() call. Therefore we avoid
* accidentally calling ->poll() when NAPI is not scheduled.
*/
work = 0;
if (test_bit(NAPI_STATE_SCHED, &n->state)) {
work = n->poll(n, weight);
trace_napi_poll(n, work, weight);
}
WARN_ON_ONCE(work > weight);
if (likely(work < weight))
goto out_unlock;
/* Drivers must not modify the NAPI state if they
* consume the entire weight. In such cases this code
* still "owns" the NAPI instance and therefore can
* move the instance around on the list at-will.
*/
if (unlikely(napi_disable_pending(n))) {
napi_complete(n);
goto out_unlock;
}
if (n->gro_bitmask) {
/* flush too old packets
* If HZ < 1000, flush all packets.
*/
napi_gro_flush(n, HZ >= 1000);
}
gro_normal_list(n);
/* Some drivers may have called napi_schedule
* prior to exhausting their budget.
*/
if (unlikely(!list_empty(&n->poll_list))) {
pr_warn_once("%s: Budget exhausted after napi rescheduled\n",
n->dev ? n->dev->name : "backlog");
goto out_unlock;
}
list_add_tail(&n->poll_list, repoll);
out_unlock:
netpoll_poll_unlock(have);
return work;
}
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
gro_result_t ret;
skb_mark_napi_id(skb, napi);
trace_napi_gro_receive_entry(skb);
skb_gro_reset_offset(skb);
ret = napi_skb_finish(dev_gro_receive(napi, skb), skb);
trace_napi_gro_receive_exit(ret);
return ret;
}
然后会直接调用netif_receive_skb_core()函数:
int netif_receive_skb_core(struct sk_buff *skb)
{
int ret;
rcu_read_lock();
ret = __netif_receive_skb_one_core(skb, false);
rcu_read_unlock();
return ret;
}
netif_receive_skb_core()调用__netif_receive_skb_one_core(),将数据包交给上层ip_rcv()进行处理:
static int __netif_receive_skb_one_core(struct sk_buff *skb, bool pfmemalloc)
{
struct net_device *orig_dev = skb->dev;
struct packet_type *pt_prev = NULL;
int ret;
ret = __netif_receive_skb_core(skb, pfmemalloc, &pt_prev);
if (pt_prev)
ret = INDIRECT_CALL_INET(pt_prev->func, ipv6_rcv, ip_rcv, skb,
skb->dev, pt_prev, orig_dev);
return ret;
}
待内存中的所有数据包被处理完成后,启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知CPU。
数据链路层和物理层的数据接收调用链如下图:
调试验证:
6. 时序图

参考资料
https://www.cnblogs.com/qishui/p/5428938.html
https://www.cnblogs.com/god-of-death/p/7152387.html
posted on 2021-01-30 03:03 overdamped 阅读(255) 评论(0) 收藏 举报
浙公网安备 33010602011771号