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最核心、最基础的功能,包括进程调度、内存管理、设备驱动管理、文件系统和网络系统等等,决定着系统的性能和稳定性。

​Linux的具体模块组成和系统架构可见下图。

Linux系统组成

1.2 Linux网络系统

1.2.1 OSI网络分层模型

开放式系统互联模型,简称为OSI模型。是一种概念模型,由国际标准化组织提出,一个试图使各种计算机在世界范围内互连为网络的标准框架。定义于ISO/IEC 7498-1。

开放式系统互联模型的开发始于上世纪70年代后期,用以支持各种计算机联网方法的出现。在上世纪80年代,该模型成为国际标准化组织(ISO)开放系统互连小组的工作产品。

OSI参考模型定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),如下图。每一层实现各自的功能和协议,并完成与相邻层的接口通信。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协议。
  • 设备无关接口层。这一层夹在网络栈和驱动层之间,网络设备种类多样,当收到数据包时,如果没有设备无关接口层的抽象,势必会导致两层之间的调用复杂,因此,有必要抽象出设备无关层,如驱动向上的传递接口,通用设备表示等。从这个设计来看,给我们很多启示,联想上面的协议无关接口层,可以看出,在一对多这种情况下,设计一个通用层会有很多好处。

614525-20170531223902993-796803885.png

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 协议栈涉及的主要函数和数据结构

img

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协议的书写,用户只需理解接口即可。

img

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_socketcallsys_socketcall几乎是用户进程socket所有操作函数的入口。

int serverSocketFd = socket(AF_INET, SOCK_STREAM, 0); //protocol为0,自动选择type对应的默认协议`

代码执行流程:

  1. net/Socket.c:sys_socketcall(),根据系统调用号,创建socket会执行sys_socket()函数;
  2. 分配socket结构,依次调用net/Socket.c:sys_socket()->sock_create()->__sock_create()->sock_alloc(),在socket文件系统中创建i节点inode = new_inode(sock_mnt->mnt_sb)
  3. 创建socket专用inode,分配inode会调用sock_alloc_inode函数来完成,实际上分配了一个socket_alloc结构体,该结构体包含socket和inode,但最终返回的是该结构体中的inode成员。至此,socket结构和inode结构均分配完毕;
  4. 根据inode取得socket对象;
  5. 使用协议族来初始化socket:
    1. 注册AF_INET协议域。初始化入口net/ipv4/Af_inet.c:这里调用sock_register()函数来完成注册,根据family将AF_INET协议域inet_family_ops注册到内核中的net_families数组中。
    2. 套接字类型。在相同的协议域下,可能会存在多个套接字类型;如AF_INET域下存在流套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW),在这三种类型的套接字上建立的协议分别是TCP, UDP,ICMP/IGMP等。AF_IENT域的这三种套接字类型定义用结构体inet_protosw(net/ipv4/Af_inet.c)来表示。
    3. 使用协议域来初始化socket。
  6. 分配sock结构以及建立socket结构与sock结构的关系;
  7. 使用tcp协议初始化sock。inet_create()函数最后,调用的是tcp_prot的init钩子函数net/ipv4/Tcp_ipv4.c:tcp_v4_init_sock(),主要是对tcp_sockinet_connection_sock进行一些初始化;
  8. socket与文件系统关联。创建好与socket相关的结构后,需要与文件系统关联,具体在sock_map_fd()函数中完成。socket与文件系统关联后,以后便可以通过文件系统read/write对socket进行操作了。

socket创建过程的调用链如下图:

socket创建调用链

socket创建过程中的数据结构如图:

image-20210129010537902

调试验证:

image-20210129013727014

2.2.3 socket发送数据——send

​ 在创建完 socket 之后, 不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。

​ 由于socket所有操作都是通过系统调用sys_socketcall()进入内核态的,所以首先要确定send()操作的内核入口程序。

image-20210129015439553

​ 可以发现,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()在应用层中的调用链应该是这样的:

image-20210129161002901

调试验证:

image-20210129154122152

2.2.4 socket接收数据——recv

recv()函数与send()函数类似,调用recv()后,通过系统调用sys_socketcall()进入内核态,在源码中可以找到recv()对应的系统调用的入口代码:

image-20210129155627877

可以看到,与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()

image-20210129161359468

继续看sock_recvmsg_nosec()

image-20210129161554328

间接调用了inet_recvmsg()

image-20210129161330356

在这里调用tcp_recvmsg()进入传输层。

总结一下,recv()在应用层中的调用链应该是这样的:

image-20210129161847128

调试验证:

image-20210129162136927

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在传输层的数据发送调用链如下图:

image-20210129215356574

调试验证:

image-20210129215807059 image-20210129215904667

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

这里共维护了三个队列:prequeuebacklogreceive_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()

image-20210129223501988

在 IP 层处理本地数据包时,会获取到上述结构的实例,并且调用实例的 handler 回调,也就是调用了tcp_v4_rcv()

tcp_v4_rcv ()函数只要做以下几个工作:

  1. 设置 TCP_CB
  2. 查找控制块
  3. 根据控制块状态做不同处理,包括 TCP_TIME_WAIT 状态处理,TCP_NEW_SYN_RECV 状态处理,TCP_LISTEN 状态处理
  4. 接收 TCP 段。

image-20210129223814239

image-20210129223915417

可以看到具体过程是检测连接状态最后调用具体的接收处理函数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在传输层的数据接收调用链如下图:

image-20210130024531736

调试验证:

image-20210129230126500 image-20210129230056282

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层的数据发送调用链如下图:

image-20210129232932640

调试验证:

image-20210129233252844 image-20210129233315066

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协议数据接收的调用链如下图:

image-20210130001910005

调试验证:

image-20210130002045852

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 了。

数据链路层和物理层的数据发送调用链如下图:

image-20210130004346033

调试验证:

image-20210130004630052

5.2 数据接收

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

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

  3. netif_receive_skb()根据注册在全局数组ptype_allptype_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。

数据链路层和物理层的数据接收调用链如下图:

image-20210130010407952

调试验证:

image-20210130010349588

6. 时序图

参考资料

https://www.cnblogs.com/qishui/p/5428938.html

https://www.cnblogs.com/god-of-death/p/7152387.html

http://www.bubuko.com/infodetail-3339127.html

https://www.cnblogs.com/hyd-desert-camel/p/3536339.html

posted on 2021-01-30 03:03  overdamped  阅读(255)  评论(0)    收藏  举报

导航