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

1. Linux基础概念

1.1 Linux中断处理

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

1.1.1 中断的概念

  • 中断是指由于接收到来自外围硬件(相对于*处理器和内存)的异步信号或来自软件的同步信号,而进行相应的硬件/软件处理。发出这样的信号称为进行中断请求(interrupt request,IRQ)。硬件中断导致处理器通过一个上下文切换(context switch)来保存执行状态(以程序计数器和程序状态字等寄存器信息为主);软件中断则通常作为CPU指令集中的一个指令,以可编程的方式直接指示这种上下文切换,并将处理导向一段中断处理代码。中断在计算机多任务处理,尤其是实时系统中尤为有用。这样的系统,包括运行于其上的操作系统,也被称为“中断驱动的”(interrupt-driven)。
  • 中断是一种使CPU中止正在执行的程序而转去处理特殊事件的操作,这些引起中断的事件称为中断源,它们可能是来自外设的输入输出请求,也可能是计算机的一些异常事故或其它内部原因。
  • 中断:在运行一个程序的过程中,断续地以“插入”方式执行一些完成特定处理功能的程序段,这种处理方式称为中断。

1.1.2 中断的作用

  • 并行操作
  • 硬件故障报警与处理
  • 支持多道程序并发运行,提高计算机系统的运行效率
  • 支持实时处理功能

1.1.3 中断处理步骤

(1)中断请求:中断源向CPU发出中断请求
(2)中断响应
(3)保护断点和现场:以便在中断服务程序执行后正确的返回主程序。
(4)中断处理
(5)中断返回

1.2 softirq、tasklet、work queue

​ Linux把中断过程中的要执行的任务分为紧急、 非紧急、非紧急可延迟三类。在中断处理程序中只完成不可延迟的部分以达到快速响应的目的,并把可延迟的操作内容推迟到内核的下半部分执行,在一个更合适的时机调用函数完成这些可延迟的操作。Linux内核提供了软中断softirq、tasklet和工作队列work queue等方法进行实现。

1.2.1 softirq

  软件中断(softIRQ)是内核提供的一种延迟执行机制,它完全由软件触发,虽然说是延迟机制,实际上,在大多数情况下,它与普通进程相比,能得到更快的响应时间。

1.2.2 tasklet

  tasklet是IO驱动程序实现可延迟函数的首选,建立在HI_SOFTIRQ和TASKLET_SOFTURQ等软中断之上,tasklet和高优先级的tasklet分别存放在tasklet_vec和tasklet_hi_vec数组中,分别由tasklet_action和tasklet_hi_action处理。

1.2.3 work queue

  work queue工作队列把工作推后,交给一个内核线程执行,工作队列允许被重新调度甚至睡眠。在Linux中的数据结构是workqueue_struct和cpu_workqueue_struct。

2. linux网络内核

2.1 OSI体系架构

TCP/IP协议体系是当前互联网的通信协议标准,在Linux网络内核中得到完整实现。在描述TCP/IP内核实现之前,先介绍作为著名的理论模型OSI体系架构,OSI体系架构常被用来评价其他协议体系,它由七个层次组成:

  • 应用层,为网络用户提供各种服务,例如电子邮件、文件传输等。表示层,为不同主机间的通信提供统一的数据表示形式。
  • 会话层,负责信息传输的组织和协调,管理进程会话过程。
  • 传输层,管理网络通信两端的数据传输,提供可靠或不可靠的传输服务。
  • 网络层,负责数据传输的路由选择和网际互连。
  • 数据链路层,负责物理相邻(通过网络介质相连)的主机间的数据传输,主要作用包括物理地址寻址、数据帧封装、差错控制等。该层可分为逻辑链路控制子层(LLC)和介质访问控制子层(MAC)。
  • 物理层,负责把主机中的数据转换成电信号,再通过网络介质(双绞线、光纤、无线信道等)来传输。该层描述了通信设备的机械、电气、功能等特性。

2.2 TCP/IP协议体系架构

相比于OSI体系,TCP/IP协议体系的架构更加简单实用。该体系包括下面四个层次:

  • 应用层,对应OSI传输层之上的层次,包括提供文件传输服务的FTP协议,提供万维网服务的HTTP协议,提供电子邮件服务的SMTP协议等。
  • 传输层,对应OSI传输层,包括TCP和UDP协议。其中,TCP提供可靠的面向连接的传输,UDP提供不可靠的无连接的传输。
  • 网络层,对应OSI网络层,包括IPv4和IPv6两种网络互连协议。
  • 网络介质层,对应OSI最低两层。该层以网络设备驱动的形式实现网络上层协议与底层介质之间的关联。在局域网上,该层包括以太网协议、802.11b协议等。

除了上述标准的四个层次外,TCP/IP协议体系还需要路由协议来管理数据传输路径,ARP协议来管理本地网络寻址。

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

2.3 Linux网络内核的组成模块

​ Linux网络内核参照网络协议体系实现了网络互连功能。

  1. 套接字接口。网络内核最顶层是支持应用程序开发的函数接口,这是一系列标准函数(目前有BSD、Posix等标准),即套接字接口(套接字的概念见第3章)。套接字支持多种不同类型的协议族:UNIX域协议族、TCP/IP协议族、IPX协议族等。本书只讨论TCP/IP协议族对应的套接字(INET套接字),该套接字又包括3个基本类型:SOCK_STREAM、SOCK_DGRAM和SOCK_RAW。通过SOCK_STREAM套接字可以访问TCP协议、SOCK_DGRAM套接字可以访问UDP协议、SOCK_RAW套接字可以直接访问IP协议。可见,套接字接口是网络内核的入口。
  2. 传输层和网络层。套接字往下依次是传输层和网络层。传输层包括标准的TCP和UDP协议模块,而网络层包括标准的IP协议模块。
  3. 数据链路层。对于需要逻辑链路的网络,数据链路层提供独立的逻辑链路协议模块,比如PPP、SLIP等。对于以太网,该层比较简单,主要的以太网协议实现被集成到底层的网卡驱动中。
  4. 网络设备驱动。由于物理特性的差异,因此不同的网络设备采用不同的设备驱动。比如,TTY设备和以太网卡采用各自的驱动模块。

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

​ 网络内核模块的实现源码分布在不同的系统文件夹下,

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

​ 除了上述模块外,还有一些被各模块访问的数据类型和接口函数,例如套接字缓冲区类型被定义在include/linux/skbuff.h;接口函数dev_queue_xmit被定义在net/core/dev.c。

2.4 内核中的数据包处理流程

​ Linux系统中,数据包的发送和接收流程大致如下图所示。图中这些函数组成的流程足以反映网络内核处理数据的过程,向下的实线箭头是数据包发送流程,向上的实线箭头是数据包接收流程,虚线箭头是路由查找流程。

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

2.4.1 数据包发送流程

​ 该流程从网络应用程序的数据包发送函数开始(如套接字接口函数sendto),到网络设备驱动的数据包发送函数结束(如CS89x网卡的函数net_send_packet)。

  1. 传输层处理。如果采用UDP协议发送,数据包由sendto经套接字接口进入UDP协议模块的udp_sendmsg。完成UDP协议封装后,再经ip_push_pending_frames进入IP协议模块。如果采用TCP协议发送,数据包由sendto经套接字接口进入TCP协议模块的tcp_sendmsg。完成TCP协议封装后,再经ip_queue_xmit进入IP协议模块。为能找到正确的数据发送路径,需要调用路由模块的ip_route_output_flow来查找路由信息。
  2. 网络层处理。UDP和TCP处理后的数据包进入IP协议模块的ip_output。确定发送出口设备后,数据包经ip_finish_output2和邻居子系统进入dev_queue_xmit接口函数。通过该函数,数据包进入网络设备。
  3. 网络设备处理。根据底层网络设备的类型,dev_queue_xmit函数把数据包交给设备的发送函数。如果底层设备是CS89x网卡,数据包由net_send_packet函数进入网络。发送前,数据包会被封装成标准帧格式。对于采用了逻辑链路协议的网络,dev_queue_xmit函数会把数据包交给逻辑链路协议模块,再经底层的网络设备发送出去。

2.4.2 数据包接收流程

​ 该流程从网络设备驱动的数据包接收函数开始(如CS89x网卡的函数net_rx),到网络应用程序的数据包接收函数结束(如套接字接口函数recvfrom)。

  1. 网络设备处理。数据包到达网络设备后,硬件中断会被触发来完成数据的接收工作。中断处理程序调用net_rx对数据包做进一步处理。解析出帧中封装的内容后,数据包由ip_rcv函数递交给IP协议模块。如果网络设备上层是逻辑链路协议模块,那么数据包必须先被递交到其协议模块,在完成处理后再由ip_rcv函数递交给IP协议模块。
  2. 网络层处理。数据包进入IP协议模块后,ip_rcv_finish首先判断数据包是本地接收的数据包还是转发的数据包。如果是本地接收的数据包,会进入ip_local_deliver函数完成IP协议的进一步处理;从IP分组解析出数据内容后,数据包会被ip_local_deliver_finish函数递交给传输层的接收函数(TCP协议是tcp_v4_rcv函数,UDP协议是udp_rcv函数)。如果ip_rcv_finish判断是转发的数据包,需要调用路由模块的ip_route_input查找路由表,确定数据转发路径;然后,将数据包交给ip_forward函数,再由ip_forward_finish进入ip_output,为转发数据包做准备。这是IP层的转发过程,从函数ip_rcv_finish开始,到ip_output结束。
  3. 传输层处理。如果采用UDP协议接收,数据包由udp_rcv函数进入UDP协议模块,再由udp_recvmsg经套接字接口进入应用程序的recvfrom函数。如果采用TCP协议接收,数据包由tcp_v4_rcv函数进入TCP协议模块,再由tcp_recvmsg经套接字接口进入网络应用程序的recvfrom函数。

3.socket简介

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

​ 说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层(在OSI模型中,主要位于会话层和传输层之间),它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议,而不需要让用户自己去定义什么时候需要指定哪个协议哪个函数。

​ socket编程中,tcp的客户端和服务端的大致流程如下图所示:

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

3.1 socket编程中的函数简单介绍

函数名 返回值 功能描述
int socket(int protofamily, int type, int protocol) 成功返回socket文件描述符,失败返回-1 socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 成功返回0;失败返回-1 为套接字指明一个本地端点地址TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号,服务器使用它来指明熟知的端口号,然后等待连接
int listen(int sockfd,int input_queue_size) 成功返回0;失败返回-1 面向连接的服务器使用它将一个套接字置为被动模式,并准备接收传入连接。用于服务器,指明某个套接字连接是被动的
int accept(int sockfd, struct sockaddr *addr, int *addrlen) 成功返回新的连接的套接字描述符;失败返回-1 获取传入连接请求,返回新的连接的套接字描述符
int connect(int sockfd,struct sockaddr *server_addr,int sockaddr_len) 成功返回0;失败返回-1 客户端通过调用connect函数来建立与TCP服务器的连接
int send(int sockfd, const void * data, int data_len, unsigned int flags) 如果成功,就返回实际copy的字节数;如果s出现错误,返回SOCKET_ERROR 在TCP连接上发送数据
int recv(int sockfd, void *buf, int buf_len,unsigned int flags) recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR 从TCP接收数据
int close(int sockfd) 失败返回-1 lose一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

3.2 调试源码

​ TCP/IP运行时序分析是基于以下源码进行gdb调试。

3.2.1 服务端源码

#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的请求接收队列长度 */
#define MAXDATASIZE 100
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");
                int numbytes;
                char buf[MAXDATASIZE];
                  if ((numbytes = recv(new_fd, buf, MAXDATASIZE, 0)) == -1)
      {
        perror("recv");
        exit(1);
    }

    buf[numbytes] = '\0';
    printf("Received: %s", buf);
            close(new_fd);
            exit(0);
        }

        close(new_fd);

        /*清除所有子进程 */
        while (waitpid(-1, NULL, WNOHANG) > 0);
    }
    close(sockfd);
    return true;
}

3.2.2 客户端代码

#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);
    
    if (send(sockfd, "Hi, world!\n", 14, 0) == -1)
                perror("send");
    
    close(sockfd);

    return true;
}

4. 发送端运行时序分析

4.1 应用层

​ 网络应用通过调用Socket API socket (int family, int type, int protocol) 创建自己的socket,该调用最终会调用 Linux system call socket() ,并最终调用 Linux Kernel 的 sock_create() 方法。总之初始化好自己的socket,send()函数流程大致如下:

  1. 调用send函数,内核封装send()为sendto(),然后发起系统调用__sys_sendto。
  2. 然后在__sys_sendto中会创建一些需要的结构体,如struct msghdr。最终调用sock_sendmsg()。
  3. sock_sendmsg()先用security_socket_sendmsg()函数用进行安全性检查,然后调用sock_sendmsg_nosec()函数。
  4. sock_sendmsg_nosec()函数中调用了inet_sendmsg()函数。
  5. inet_sendmsg()会调用socket中的sk->sk_prot中的sendmesg()函数(本次的实验用的是tcp,即调用了struct prot tcp_prot中的tcp_sendmsg()函数)进入运输层,而sk->sk_prot是在最开始的socket初始化中完成赋值的。

4.1.1 源码分析

SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
		unsigned int, flags, struct sockaddr __user *, addr,
		int, addr_len)
{
	return __sys_sendto(fd, buff, len, flags, addr, addr_len);
}

/*
 *	Send a datagram down a socket.
 */

SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
		unsigned int, flags)
{
	return __sys_sendto(fd, buff, len, flags, NULL, 0);
}

​ 在net/socket.c中定义了sys_sendto函数,当调用send()函数时,内核封装send()为sendto(),然后发起系统调用。其实也很好理解,send()就是sendto()的一种特殊情况,而sendto()在内核的系统调用服务程序为sys_sendto。

/*
 *    Send a datagram to a given address. We move the address into kernel
 *    space and check the user space data area is readable before invoking
 *    the protocol.
 */
//net/socket.c
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;
}

​ 在socket.h中定义了一个struct msghdr msg,他是用来表示要发送的数据的一些属性。

/* Structure describing messages sent by
   `sendmsg' and received by `recvmsg'.  */
struct msghdr
  {
    void *msg_name;        /* Address to send to/receive from.  */
    socklen_t msg_namelen;    /* Length of address data.  */

    struct iovec *msg_iov;    /* Vector of data to send/receive into.  */
    size_t msg_iovlen;        /* Number of elements in the vector.  */

    void *msg_control;        /* Ancillary data (eg BSD filedesc passing). */
    size_t msg_controllen;    /* Ancillary data buffer length.
                   !! The type should be socklen_t but the
                   definition of the kernel is incompatible
                   with this.  */

    int msg_flags;        /* Flags on received message.  */
  };

​ __sys_sendto函数其实做了3件事:

  1.通过fd获取了对应的struct socket

  2.创建了用来描述要发送的数据的结构体struct msghdr。

  3.调用了sock_sendmsg来执行实际的发送。

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

/**
 *    sock_sendmsg - send a message through @sock
 *    @sock: socket
 *    @msg: message to send
 *
 *    Sends @msg through @sock, passing through LSM.
 *    Returns the number of bytes sent, or an error code.
 */
//net/socket.c
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);

​ 通过查阅资料,可以的看到在sock_sendmsg()函数中,为了在发送多个消息时,提高性能会调用sock_sendmsg_nosec(),这是为了跳过上面的安全检查,提高性能。简单检查后就进入__sock_sendmsg_nosec()。

//net/socket.c
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。

//net/ipv4/af_inet.c
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);
}


//include/linux/indirect_call_wrapper.h
#define INDIRECT_CALL_1(f, f1, ...)					\
	({								\
		likely(f == f1) ? f1(__VA_ARGS__) : f(__VA_ARGS__);	\
	})
#define INDIRECT_CALL_2(f, f2, f1, ...)					\
	({								\
		likely(f == f2) ? f2(__VA_ARGS__) :			\
				  INDIRECT_CALL_1(f, f1, __VA_ARGS__);	\
	})

​ 从上述代码和宏定义中可以发现,函数是间接调用了tcp_sendmsg或者udp_sendmsg,而这取决于传入的sock->sk->sk_prot中的类型,在socket初始化中,已经将sk_prot定义为tcp_prot,也就是说将会执行tcp_sendmsg()函数,即传送到传输层。

4.1.2 gdb调试验证

​ 断点设置如下:

b __sys_sendto
b sock_sendmsg
b security_socket_sendmsg
b sock_sendmsg_nosec
b inet_sendmsg
b tcp_sendmsg

​ 此时的调用堆栈:

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

​ 如上述gdb调试结果,和我们的预期是一致的,所以也大概清楚了应用层的数据发送的时序流程。

4.2 传输层

在传输层中,第一个入口函数是上层调用的tcp_sendmsg,然后再进行函数调用,流程如下所示:

  1. tcp_sendmsg() 函数,首先该函数上锁,然后调用tcp_sendmsg_locked()函数
  2. tcp_sendmsg_locked()处理用户数据的存放,将所有的数据组织成发送队列,然后调用tcp_push()
  3. tcp_push()中涉及到小包阻塞的问题,并且会判断这个skb的元素是否需要push,如果需要就将tcp头部字段的push置一,然后执行__tcp_push_pending_frames(sk, mss_now, nonagle)
  4. __tcp_push_pending_frames(sk, mss_now, nonagle)函数会发出任何由于TCP_CORK或者尝试合并小数据包而被延迟挂起的帧,发送使用tcp_write_xmit()函数。
  5. tcp_write_xmit()实现了tcp的拥塞控制,然后调用了tcp_transmit_skb(sk, skb, 1, gfp)传输数据。
  6. tcp_transmit_skb(sk, skb, 1, gfp)实际调用了__tcp_transmit_skb(sk, skb, 1, gfp)
  7. __tcp_transmit_skb(sk, skb, 1, gfp)函数会构造tcp的头内容,计算校验和等等,然后调用icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl)函数,进入网络层

4.2.1 源码分析

​ 从上面所说的应用层,我们可以得知最后到达传输层的tcp协议的函数为tcp_sendmsg,所以在代码中查看该函数:

//net/ipv4/tcp.c
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;

}

EXPORT_SYMBOL(tcp_sendmsg);

​ 从这段代码可以看出,发送的过程涉及到上锁和释放锁的一个操作,查阅资料可知目的是让接收和发送队列能够有序进行相关的工作。然后主要的发送函数即为tcp_sendmsg_locked这个函数,继续追踪该函数,

//net/ipv4/tcp.c
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;
	int flags, err, copied = 0;
	int mss_now = 0, size_goal, copied_syn = 0;
	int process_backlog = 0;
	bool zc = false;
	long timeo;

	......

wait_for_sndbuf:
		set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
    //如果已经有数据拷贝至发送队列,那就先把这部分发送,再等待内存释放
		if (copied)
			tcp_push(sk, flags & ~MSG_MORE, mss_now,
				 TCP_NAGLE_PUSH, size_goal);

		err = sk_stream_wait_memory(sk, &timeo);
		if (err != 0)
			goto do_error;

		mss_now = tcp_send_mss(sk, &size_goal, flags);
	}

out:
	if (copied) {
		tcp_tx_timestamp(sk, sockc.tsflags);
		tcp_push(sk, flags, mss_now, tp->nonagle, size_goal);
	}
out_nopush:
	sock_zerocopy_put(uarg);
	return copied + copied_syn;

do_error:
	skb = tcp_write_queue_tail(sk);
do_fault:
	tcp_remove_empty_skb(sk, skb);

	if (copied + copied_syn)
		goto out;
out_err:
	sock_zerocopy_put_abort(uarg, true);
	err = sk_stream_error(sk, flags, err);
	/* make sure we wake any epoll edge trigger waiter */
	if (unlikely(tcp_rtx_and_write_queues_empty(sk) && err == -EAGAIN)) {
		sk->sk_write_space(sk);
		tcp_chrono_stop(sk, TCP_CHRONO_SNDBUF_LIMITED);
	}
	return err;
}

​ 这个函数的代码相当多,涉及到的逻辑即为tcp协议中具体的几个处理过程。首先即为检查连接状态的TCPF_ESTABLISHED和TCPF_CLOSE_WAIT,如果不是这两个状态,即代表要等待连接或者进行出错处理;然后就是检查数据的是否分段,获取最大的MSS数据,将数据复制到skb队列进行发送。当然,这两部中间省略了很多检查的逻辑,也做了很多出错的处理,作为正确发送的过程中,这两个倒不是特别重要,所以先不详述。到发送这一步,可以看到tcp_push函数,这个即为将数据添加到sk队列追踪,检查是否立即发送,然后将数据复制到发送队列中,一切正常的话,即进行发送。在查看tcp_push函数,

//net/ipv4/tcp.c
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
	skb = tcp_write_queue_tail(sk);
	if (!skb)
		return;
    //判断是否需要设置PSH标记
	if (!(flags & MSG_MORE) || forced_push(tp))
		tcp_mark_push(tp, skb);
	//如果设置了MSG_OOB选项,就记录紧急指针
	tcp_mark_urg(tp, flags);
	//判断是否需要阻塞小包,将多个小数据段合并到一个skb中一起发送
	if (tcp_should_autocork(sk, skb, size_goal)) {

		/* avoid atomic op if TSQ_THROTTLED bit is already set */
        
        //设置TSQ_THROTTLED标志
		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.
		 */
        //有可能在设置TSQ_THROTTLED前,网卡txring已经完成发送
        //因此再次检查条件,避免错误阻塞数据报文
		if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize)
			return;
	}
    //应用程序用MSG_MORE标识告诉4层将会有更多的小数据包的传输
    //然后将这个标记再传递给3层,3层就会提前划分一个mtu大小的数据包,来组合这些数据帧
	if (flags & MSG_MORE)
		nonagle = TCP_NAGLE_CORK;
	//TCP层还没处理完,接着往下走
	__tcp_push_pending_frames(sk, mss_now, nonagle);
}

​ 此处已经通过代码写入到skb队列当中,最后调用了 __tcp_push_pending_frames这个函数,这个时候通过这个函数进入到tcp_output.c这个文件,即为具体的处理过程。

/* net/ipv4/tcp_output.c
 * Push out any pending frames which were held back due to
 * TCP_CORK or attempt at coalescing tiny packets.
 * The socket must be locked by the caller.
 */
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
			       int nonagle)
{
	/* If we are closed, the bytes will have to remain here.
	 * In time closedown will finish, we empty the write queue and
	 * all will be happy.
	 */
    //如果连接处于关闭状态,返回不处理
	if (unlikely(sk->sk_state == TCP_CLOSE))
		return;
	//tcp_write_xmit会再次判断是否要推迟数据发送,使用nagle算法
	if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
			   sk_gfp_mask(sk, GFP_ATOMIC)))
        //发送失败,检测是否需要开启零窗口探测定时器
		tcp_check_probe_timer(sk);
}

再进入tcp_write_xmit()发送数据,源码如下:

//net/ipv4/tcp_output.c
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;

	tcp_mstamp_refresh(tp);
    //如果只发送一个数据报文,则不做MTU探测
	if (!push_one) {
		/* Do MTU probing. */
		result = tcp_mtu_probe(sk);
		if (!result) {
			return false;
		} else if (result > 0) {
			sent_pkts = 1;
		}
	}

	max_segs = tcp_tso_segs(sk, mss_now);
    //若发送队列未满,则准备发送报文
	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;
		}

		if (tso_segs == 1) {
			if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
						     (tcp_skb_is_last(sk, skb) ?
						      nonagle : TCP_NAGLE_PUSH))))
				break;
		} else {
			if (!push_one &&
			    tcp_tso_should_defer(sk, skb, &is_cwnd_limited,
						 &is_rwnd_limited, max_segs))
				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, skb, limit, mss_now, gfp)))
			break;

		if (tcp_small_queue_check(sk, skb, 0))
			break;

		/* Argh, we hit an empty skb(), presumably a thread
		 * is sleeping in sendmsg()/sk_stream_wait_memory().
		 * We do not want to send a pure-ack packet and have
		 * a strange looking rtx queue with empty packet(s).
		 */
		if (TCP_SKB_CB(skb)->end_seq == TCP_SKB_CB(skb)->seq)
			break;

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

	......
}

​ tcp_write_xmit这个函数为具体发送过程,检查连接状态和拥塞窗口的大小,然后将skb队列发送出去。再往下看可以追踪到tcp_transmit_skb函数和__tcp_transmit_skb函数等,

//net/ipv4/tcp_output.c
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
			    gfp_t gfp_mask)
{
	return __tcp_transmit_skb(sk, skb, clone_it, gfp_mask,
				  tcp_sk(sk)->rcv_nxt);
}

//net/ipv4/tcp_output.c
/* This routine actually transmits TCP packets queued in by
 * tcp_do_sendmsg().  This is used by both the initial
 * transmission and possible later retransmissions.
 * All SKB's seen here are completely headerless.  It is our
 * job to build the TCP header, and pass the packet down to
 * IP so it can do the same plus pass the packet off to the
 * device.
 *
 * We are working here with either a clone of the original
 * SKB, or a fresh unique copy made by the retransmit engine.
 */
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
			      int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
	const struct inet_connection_sock *icsk = inet_csk(sk);
	struct inet_sock *inet;
	struct tcp_sock *tp;
	struct tcp_skb_cb *tcb;
	struct tcp_out_options opts;
	unsigned int tcp_options_size, tcp_header_size;
	struct sk_buff *oskb = NULL;
	struct tcp_md5sig_key *md5;
	struct tcphdr *th;
	u64 prior_wstamp;
	int err;
	......

	/* Build TCP header and checksum it. 构建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);
	*(((__be16 *)th) + 6)	= htons(((tcp_header_size >> 2) << 12) |
					tcb->tcp_flags);

	th->check		= 0;
	th->urg_ptr		= 0;

	/* The urg_mode check is necessary during a below snd_una win probe */
	if (unlikely(tcp_urg_mode(tp) && before(tcb->seq, tp->snd_up))) {
		if (before(tp->snd_up, tcb->seq + 0x10000)) {
			th->urg_ptr = htons(tp->snd_up - tcb->seq);
			th->urg = 1;
		} else if (after(tcb->seq + 0xFFFF, tp->snd_nxt)) {
			th->urg_ptr = htons(0xFFFF);
			th->urg = 1;
		}
	}

	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));
	}
#ifdef CONFIG_TCP_MD5SIG
	/* Calculate the MD5 hash, as we have all we need now */
	if (md5) {
		sk_nocaps_add(sk, NETIF_F_GSO_MASK);
		tp->af_specific->calc_md5_hash(opts.hash_location,
					       md5, sk, skb);
	}
#endif

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

	tcp_add_tx_delay(skb, tp);

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

	if (unlikely(err > 0)) {
		tcp_enter_cwr(sk);
		err = net_xmit_eval(err);
	}
	if (!err && oskb) {
		tcp_update_skb_after_send(sk, oskb, prior_wstamp);
		tcp_rate_skb_sent(sk, oskb);
	}
	return err;
}

​ 追踪到err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl)这个语句的时候,突然眼前一亮,因为传输层追踪到最后肯定是要发往网络层的数据,一直到现在,才能看到和网络层有些关系的东西。因为对于icsk->icsk_af_ops这个函数指针,在tcp协议栈中,会被初始化为ip_queue_xmit,所以知道这个函数这里才进入了网络层。可以简述为tcp_transmit_skb函数负责将tcp数据发送出去,这里调用了icsk->icsk_af_ops->queue_xmit函数指针,实际上就是在TCP/IP协议栈初始化时设定好的IP层向上提供数据发送接口ip_queue_xmit函数,这里TCP协议栈通过调用这个icsk->icsk_af_ops->queue_xmit函数指针来触发IP协议栈代码发送数据。到这个函数为止,传输层发送过程追踪完毕。

4.2.2 gdb验证

​ 断点设置如下:

b tcp_sendmsg
b tcp_sendmsg_locked
b tcp_push
b __tcp_push_pending_frames
b tcp_write_xmit
b tcp_transmit_skb
b __tcp_transmit_skb

​ 调用栈:

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

4.3 网络层

​ 传输层最后通过__tcp_transmit_skb()函数中的icsk->icsk_af_ops->queue_xmit()进入网络层,开始执行第一个网络层函数ip_queue_xmit():

  1. ip_queue_xmit()函数就是调用了__ip_queue_xmit()。
  2. __ip_queue_xmit()函数进行路由,构造部分ip头部内容,调用ip_local_out()函数来发送数据。
  3. ip_local_out()函数就是调用了__ip_local_out()这个函数。
  4. __ip_local_out()函数设置ip头部长度,校验和,设置协议,然后放入到NF_INET_LOCAL_OUT hook点。
  5. 从 hook点出来时,进入ip_output()这个函数。
  6. ip_output()会使得数据包先通过NF_INET_POST_ROUTING的hook,然后再下发到ip_finish_output()函数中。
  7. ip_finish_output()在不分片的情况下,调用了ip_finish_output2()这个函数
  8. ip_finish_output2()这个函数检查skb的头部空间是否能够容纳下二层头部,如果空间不足,重新申请skb,然后通过通过邻居子系统neigh_output()输出。
  9. neigh_output()输出分为有二层头有缓存和没有两种情况,有缓存时调用neigh_hh_output()进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出,neigh_hh_output()
  10. neigh_hh_output()函数最终通过dev_queue_xmit()进入链路层

4.3.1 源码分析

​ 入口函数是ip_queue_xmit,ip_queue_xmit是 ip 层提供给 tcp 层发送回调函数。ip_queue_xmit()完成面向连接套接字的包输出,当套接字处于连接状态时,所有从套接字发出的包都具有确定的路由, 无需为每一个输出包查询它的目的入口,可将套接字直接绑定到路由入口上, 这由套接字的目的缓冲指针(dst_cache)来完成。ip_queue_xmit()首先为输入包建立IP包头, 经过本地包过滤器后,再将IP包分片输出(ip_fragment)。Ip_queue_xmit实际上是调用__ip_queue_xmit

//include/net/ip.h
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);
}


//net/ipv4/ip_output.c
/* 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);
    //如果存在缓存,直接跳到packet_routed位置执行否则通过ip_route_output_ports查找路由缓存,
	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);
	}
    //将路由设置到SVB中
	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. */
    //接下来我们知道了目的地址,我们就可以构造并且加入ip头部内容
	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;
}

其中Skb_rtable(skb)获取 skb 中的路由缓存,然后判断是否有缓存,如果有缓存就直接进行packet_routed函数,否则就 执行ip_route_output_ports查找路由缓存。最后调用ip_local_out发送数据包。同函数ip_queue_xmit一样,ip_local_out函数内部调用__ip_local_out。

//net/ipv4/ip_output.c
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;
}

//net/ipv4/ip_output.c
int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	struct iphdr *iph = ip_hdr(skb);
	//设置ip包总长度
	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);

    //要经过netfilter的LOCAL_HOOK hook点
	return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
		       net, sk, skb, NULL, skb_dst(skb)->dev,
		       dst_output);
}

__ip_local_out(net, sk, skb)这个函数设置了数据包的总长度和校验和,通过netfilter的LOCAL_OUT检查,如果通过,则通过dst_output发送数据包。

//include/net/dst.h
/* Output packet to network from transport.  */
static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	return skb_dst(skb)->output(net, sk, skb);
}

继续深入源码,可以发现这个函数调用了将skb转为dst_entry后的output()函数,这个output()函数首先会调用ip_output()源代码如下:

//net/ipv4/ip_output.c
int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	struct net_device *dev = skb_dst(skb)->dev;

	IP_UPD_PO_STATS(net, IPSTATS_MIB_OUT, skb->len);

	skb->dev = dev;
	skb->protocol = htons(ETH_P_IP);

	return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
			    net, sk, skb, NULL, dev,
			    ip_finish_output,
			    !(IPCB(skb)->flags & IPSKB_REROUTED));
}

与_ip_local_out(net, sk, skb)这个函数的最后类似,ip_output()函数会使得数据包先通过NF_INET_POST_ROUTING的hook,然后再下发到ip_finish_output()函数中,该函数实际上是调用了__ip_finish_output,源码如下:

//net/ipv4/ip_output.c
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;
	}
}

//net/ipv4/ip_output.c
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
	mtu = ip_skb_dst_mtu(sk, skb);
	if (skb_is_gso(skb))
		return ip_finish_output_gso(net, sk, skb, mtu);
	//如果需要分片就进行分片,否则直接调用ip_finish_output2(net, sk, skb);
	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(net, sk, skb)这个函数,源码如下

//net/ipv4/ip_output.c
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();
    //arp协议执行开始处
	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_finish_output2()函数检查skb的头部空间是否能够容纳下二层头部,如果空间不足,重新申请skb,然后通过通过邻居子系统neigh_output(neigh, skb)输出。

//include/net/neighbour.h
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()进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出,neigh_hh_output()源码如下:

//include/net/neighbour.h
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;
	//拷贝二层头到skb
	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函数进行向链路层发送包。

4.3.2 gdb验证

​ 断点设置如下:

b ip_queue_xmit
b __ip_queue_xmit
b ip_local_out
b __ip_local_out
b ip_output
b ip_finish_output
b ip_finish_output2
b neigh_output
b dev_queue_xmit

​ 调用栈:

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

4.4 网络接口层

​ 网络接口层对应着tcp/ip协议的最底层,也可以理解为是常说的数据链路层和物理层的结合,向上接收网络层来的包,向下联系具体的物理网络。在链路层的数据流为帧,在物理层转化为比特流。改层的数据流动较为简单和清晰,个人觉得这一层次的重点完全依赖于硬件的实现。

从网络层的dev_queue_xmit()进入链路层,dev_queue_xmit()为第一个函数:

  1. dev_queue_xmit()就是调用了__dev_queue_xmit()这个函数。
  2. 从对_dev_queue_xmit()函数的分析来看,发送报文有2中情况:有拥塞控制策略的情况,比较复杂,但是目前最常用;没有enqueue的状况,比较简单,直接发送到driver,如loopback等使用。先检查是否有enqueue的规则,如果有即调用__dev_xmit_skb进入拥塞控制的flow,如果没有且txq处于On的状态,那么就调用dev_hard_start_xmit()直接发送到driver。
  3. dev_hard_start_xmit()函数通过循环调用xmit_one()函数发送一个或者多个数据。
    xmit_one()会调用 netdev_start_xmit()。
  4. netdev_start_xmit()实际调用的是__netdev_start_xmit()函数,其目的就是将封包送到driver的tx函数。
  5. __netdev_start_xmit()会调用为网卡编写的驱动中的回调函数指针,从而把数据发送给网卡。

4.4.1 源码分析

上层跟踪出来的入口函数dev_queue_xmit,即在这个函数入口这里进入链路层进行处理。

//net/core/dev.c
int dev_queue_xmit(struct sk_buff *skb)

{

    return __dev_queue_xmit(skb, NULL);

}

可以看出和别的入口函数类似,调用__dev函数,_dev函数的实现较为复杂,

//net/core/dev.c
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 device/qdisc don't need skb->dst, release it right now while
	 * its hot in this cpu cache.
	 */
	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;
	}

	/* The device has no queue. Common case for software devices:
	 * loopback, all the sorts of tunnels...

	 * Really, it is unlikely that netif_tx_lock protection is necessary
	 * here.  (f.e. loopback and IP tunnels are clean ignoring statistics
	 * counters.)
	 * However, it is possible, that they rely on protection
	 * made by us here.

	 * Check this and shot the lock. It is not prone from deadlocks.
	 *Either shot noqueue qdisc, it is even simpler 8)
	 */
	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_hard_start_xmit函数向下发送数据。

//net/core/dev.c
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的下一个数据单元

		skb_mark_not_on_list(skb);
        //将此数据包送到driver Tx函数,因为dequeue的数据也会从这里发送,所以会有next
		rc = xmit_one(skb, dev, txq, next != NULL);
         //如果发送不成功,next还原到skb->next 退出
		if (unlikely(!dev_xmit_complete(rc))) {
			skb->next = next;
			goto out;
		}
		 //如果发送成功,把next置给skb,一般的next为空 这样就返回,如果不为空就继续发!
		skb = next;
        //如果txq被stop,并且skb需要发送,就产生TX Busy的问题!
		if (netif_tx_queue_stopped(txq) && skb) {
			rc = NETDEV_TX_BUSY;
			break;
		}
	}

out:
	*ret = rc;
	return skb;
}

最终的数据通过xmit_one这个函数传递给物理层的设备,到这里虚拟的传递的驱动就要结束了,将和实际的设备驱动连接起来.

//net/core/dev.c
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;
}

​ xmit_one函数在使用的过程中,利用netdev_start_xmit来启动物理层的接口,进而调用__netdev_start_xmit,物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中,同时在数据的拷贝中,还会加入相关协议等。对于以太网网络,物理层发送采用CSMA/CD协议,即在发送过程中侦听链路冲突。一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。到这一步,这个数据就可以完整的输出到物理层设备上了,转化为比特流的形式。

//include/linux/netdevice.h
static inline netdev_tx_t netdev_start_xmit(struct sk_buff *skb, struct net_device *dev,
					    struct netdev_queue *txq, bool more)
{
	const struct net_device_ops *ops = dev->netdev_ops;
	netdev_tx_t rc;

	rc = __netdev_start_xmit(ops, skb, dev, more);
	if (rc == NETDEV_TX_OK)
		txq_trans_update(txq);

	return rc;
}

//include/linux/netdevice.h
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);
}

4.4.2 gdb验证

​ 断点设置如下:

b dev_queue_xmit
b __dev_queue_xmit
b dev_hard_start_xmit
b xmit_one
b netdev_start_xmit
b __netdev_start_xmit

​ 调用栈:

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

5. 接收端运行时序分析

​ 接收端的过程较为简单,因为从物理设备上到链路层,还是很底层的东西,所以会经过很多中断处理的过程,其中就包括我们前面所说的软中断(softirq)。

5.1 网络接口层

当一个包到达物理层网络时,接收到数据帧就会引发中断。接收端中断处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。在linux5.4.34内核中,利用一组特殊的API 来处理接收的数据帧,即 NAPI,通过NAPI机制该中断处理程序调用 Network device的 netif_rx_schedule 函数,进入软中断处理流程,再调用 net_rx_action 函数。

  1. net_rx_action()在调用net_rx_action函数执行软中断NET_RX_SOFTIRQ时会遍历poll_list链表,然后调用网卡驱动注册到的napi_poll()。
  2. napi_poll()函数中会一个接一个地读取网卡中的数据包,将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用napi_gro_receive()函数。
  3. napi_gro_receive()结束后直接调用netif_receive_skb_core()。
  4. netif_receive_skb_core()调用__netif_receive_skb_one_core()。
  5. __netif_receive_skb_one_core()函数会将数据包交给上层ip_rcv()进行处理。

5.1.1 源码分析

接受数据的入口函数是net_rx_action。

//net/core/dev.c
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函数来一个一个的处理数据包。在poll函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道。驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用napi_gro_receive函数。

//net/core/dev.c
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函数。

//net/core/dev.c
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;
}
EXPORT_SYMBOL(netif_receive_skb_core);

netif_receive_skb_core调用 __netif_receive_skb_one_core,将数据包交给上层ip_rcv进行处理。

//net/core/dev.c
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;
}

5.1.2 gdb验证

​ 断点设置

b net_rx_action
b napi_poll
b __netif_receive_skb
b __netif_receive_skb_one_core
b ip_rcv

​ 运行结果

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

5.2 网络层

从ip_rcv()开始进入网络层代码执行:

  1. ip_rcv()首先会进入hook点NF_INET_PRE_ROUTING进行处理,然后通过后再调用ip_rcv_finish()函数。
  2. ip_rcv_finish()主要工作是完成路由表的查询,通过ip_rcv_finish_core()函数判断下一条的路由是需要转发,丢弃还是传给上层协议,加入传给上层协议那么调用dst_input()。
  3. 调用的input方法就是路由子系统赋的ip_local_deliver()。
  4. ip_local_deliver()在路由结束后会先判断是否为分片,有的话先整合分片,之后再传入本机的要需要再经过一个HOOK点,NF_INET_LOCAL_IN。
  5. HOOK之后通过之后执行ip_local_deliver_finish()函数。
  6. ip_local_deliver_finish()函数执行ip_protocol_deliver_rcu()函数。
  7. ip_protocol_deliver_rcu()函数调用上层的接受函数tcp_v4_rcv()。

5.2.1 源码分析

上面传输层的部分可以得知,网络层的的接收端函数的入口地址是ip_rcv,查看源码可以看到

//net/ipv4/ip_input.c
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这个函数接口,ip_rcv_finish 函数会调用dst_input函数,

//net/ipv4/ip_input.c
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;
}

//include/net/dst.h
static inline int dst_input(struct sk_buff *skb)
{
	return skb_dst(skb)->input(skb);
}

在dst_input函数中,最终在ip层即生成ip_input,根据路由选择调用*ip_router_input 函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃。

根据源码可以看出发向上层的数据时调用 ip_local_deliver 函数,可能会合并IP包,然后调用 ip_local_deliver 函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv等,对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而处理流程进入 TCP 栈。由此可以和我们刚刚追踪的传输层的函数连接起来;当然,跟新路由的时候如果是转发而不是发送到本机则向下层处理。

//net/ipv4/ip_input.c
/*
 * 	Deliver IP Packets to the higher protocol layers.
 */
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_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即为具体的选择协议的函数,

//net/ipv4/ip_input.c
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);
		}
	}
}

此处,根据tcp协议,调用了tcp_v4_rcv函数,向上进入传输层处理。

5.2.2 gdb验证

​ 断点设置

b ip_rcv
b ip_rcv_finish
b ip_rcv_finish_core
b dst_input
b ip_local_deliver
b ip_local_deliver_finish
b ip_protocol_deliver_rcu
b tcp_v4_rcv

​ 运行结果

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

5.3 传输层

  1. tcp_v4_rcv()将数据传入传输层,并且调用tcp_v4_do_rcv()。
  2. tcp_v4_do_ecv()检查状态如果是established,就调用tcp_rcv_established()函数。
  3. tcp_rcv_established用于处理已连接状态下的输入,处理过程根据首部预测字段分为快速路径和慢速路径,最终everything is ok的情况下执行tcp_data_queue()。
  4. tcp_data_queu()使用tcp_queue_rcv()将数据加入到接收队列中。
  5. 最终tcp_data_queu()会调用tcp_data_ready()提醒当前sock有数据可读事件。

5.3.1 源码分析

网络层最后通过调用tcp_v4_rcv()将数据传入传输层,我们先看tcp_v4_rcv()的源码:

//net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
	struct net *net = dev_net(skb->dev);
	struct sk_buff *skb_to_free;
	int sdif = inet_sdif(skb);
	const struct iphdr *iph;
	const struct tcphdr *th;
	bool refcounted;
	struct sock *sk;
	int ret;

	.....

	if (sk->sk_state == TCP_LISTEN) {
		ret = tcp_v4_do_rcv(sk, skb);
		goto put_and_return;
	}
    
    sk_incoming_cpu_update(sk);

	bh_lock_sock_nested(sk);
	tcp_segs_in(tcp_sk(sk), skb);
	ret = 0;
	if (!sock_owned_by_user(sk)) {
		skb_to_free = sk->sk_rx_skb_cache;
		sk->sk_rx_skb_cache = NULL;
		ret = tcp_v4_do_rcv(sk, skb);
	} else {
		if (tcp_add_backlog(sk, skb))
			goto discard_and_relse;
		skb_to_free = NULL;
	}
	.......
}

tcp_v4_rcv()函数为TCP的总入口,数据包从IP层传递上来,进入该函数。该函数主要做以下几个工作:(1) 设置TCP_CB (2) 查找控制块 (3)根据控制块状态做不同处理,包括TCP_TIME_WAIT状态处理,TCP_NEW_SYN_RECV状态处理,TCP_LISTEN状态处理 (4) 接收TCP段;

我们主要查看处于LISTEN状态下的情况,可以发现在LISTEN情况下,该函数会调用 tcp_v4_do_rcv();如果是其他状态,将TCP包投递到目的套接字进行接收处理。如果套接字未被上锁则调用tcp_v4_do_rcv()。当套接字正被用户锁定,TCP包将暂时排入该套接字的后备队列(sk_add_backlog)。我们查看tcp_v4_do_rcv()源码:

//net/ipv4/tcp_ipv4.c
/* The socket must have it's spinlock held when we get
 * here, unless it is a TCP_LISTEN socket.
 *
 * We have a potential double-lock case here, so even when
 * doing backlog processing we use the BH locking scheme.
 * This is because we cannot sleep with the original spinlock
 * held.
 */
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_checksum_complete(skb))
		goto csum_err;

	if (sk->sk_state == TCP_LISTEN) {
		struct sock *nsk = tcp_v4_cookie_check(sk, skb);

		if (!nsk)
			goto discard;
		if (nsk != sk) {
			if (tcp_child_process(sk, nsk, skb)) {
				rsk = nsk;
				goto reset;
			}
			return 0;
		}
	} else
		sock_rps_save_rxhash(sk, skb);

	if (tcp_rcv_state_process(sk, skb)) {
		rsk = sk;
		goto reset;
	}
	return 0;

reset:
	tcp_v4_send_reset(rsk, skb);
discard:
	kfree_skb(skb);
	/* Be careful here. If this function gets more complicated and
	 * gcc suffers from register pressure on the x86, sk (in %ebx)
	 * might be destroyed here. This current version compiles correctly,
	 * but you have been warned.
	 */
	return 0;

csum_err:
	TCP_INC_STATS(sock_net(sk), TCP_MIB_CSUMERRORS);
	TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS);
	goto discard;
}

tcp_v4_do_ecv()检查状态如果是established,就调用tcp_rcv_established()函数,源码如下:

//net/ipv4/tcp_input.c
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;

	/* TCP congestion window tracking */
	trace_tcp_probe(sk, skb);

	tcp_mstamp_refresh(tp);
	if (unlikely(!sk->sk_rx_dst))
		inet_csk(sk)->icsk_af_ops->sk_rx_dst_set(sk, skb);
    
    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;

		/* Timestamp header prediction: tcp_header_len
		 * is automatically equal to th->doff*4 due to pred_flags
		 * match.
		 */

		/* Check timestamp */
		if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {
			/* No? Slow path! */
			if (!tcp_parse_aligned_timestamp(tp, th))
				goto slow_path;

			/* If PAWS failed, check it more carefully in slow path */
			if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)
				goto slow_path;

			/* DO NOT update ts_recent here, if checksum fails
			 * and timestamp was corrupted part, it will result
			 * in a hung connection since we will drop all
			 * future packets due to the PAWS test.
			 */
		}

		if (len <= tcp_header_len) {
			/* Bulk data transfer: sender */
			if (len == tcp_header_len) {
				/* Predicted packet is in window by definition.
				 * seq == rcv_nxt and rcv_wup <= rcv_nxt.
				 * Hence, check seq<=rcv_wup reduces to:
				 */
				if (tcp_header_len ==
				    (sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
				    tp->rcv_nxt == tp->rcv_wup)
					tcp_store_ts_recent(tp);

				/* We know that such packets are checksummed
				 * on entry.
				 */
				tcp_ack(sk, skb, 0);
				__kfree_skb(skb);
				tcp_data_snd_check(sk);
				/* When receiving pure ack in fast path, update
				 * last ts ecr directly instead of calling
				 * tcp_rcv_rtt_measure_ts()
				 */
				tp->rcv_rtt_last_tsecr = tp->rx_opt.rcv_tsecr;
				return;
			} else { /* Header too small */
				TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS);
				goto discard;
			}
		} 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;

			/* Predicted packet is in window by definition.
			 * seq == rcv_nxt and rcv_wup <= rcv_nxt.
			 * Hence, check seq<=rcv_wup reduces to:
			 */
			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);
            ........
}

tcp_rcv_established用于处理已连接状态下的输入,处理过程根据首部预测字段分为快速路径和慢速路径。

  1. 快速路径:用于处理预期的,理想情况下的数据段,在这种情况下,不会对一些边缘情形进行检测,进而达到快速处理的目的;

    无数据的情况下:若无数据,则处理输入ack,释放该skb,检查是否有数据发送,有则发送;有数据的情况下:

    有数据,则使用tcp_queue_rcv()将数据加入到接收队列中。

  2. 慢速路径:用于处理那些非预期的,非理想情况下的数据段,即不满足快速路径的情况下数据段的处理;这种情况下会进行更详细的校验,然后处理ack,处理紧急数据,接收数据段,其中数据段可能包含乱序的情况,最后进行是否有数据和ack的发送检查;

最终正如该函数的最上方的注释所言,everything is ok下会调用tcp_data_queue(),源码如下:

//net/ipv4/tcp_input.c
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
	struct tcp_sock *tp = tcp_sk(sk);
	bool fragstolen;
	int eaten;

	if (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq) {
		__kfree_skb(skb);
		return;
	}
	skb_dst_drop(skb);
	__skb_pull(skb, tcp_hdr(skb)->doff * 4);

	tcp_ecn_accept_cwr(sk, skb);

	tp->rx_opt.dsack = 0;

	/*  Queue data for delivery to the user.
	 *  Packets in sequence go to the receive queue.
	 *  Out of sequence packets to the out_of_order_queue.
	 */
	if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
		if (tcp_receive_window(tp) == 0) {
			NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPZEROWINDOWDROP);
			goto out_of_window;
		}

		/* Ok. In sequence. In window. */
queue_and_out:
		if (skb_queue_len(&sk->sk_receive_queue) == 0)
			sk_forced_mem_schedule(sk, skb->truesize);
		else if (tcp_try_rmem_schedule(sk, skb, skb->truesize)) {
			NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPRCVQDROP);
			goto drop;
		}

		eaten = tcp_queue_rcv(sk, skb, &fragstolen);
		if (skb->len)
			tcp_event_data_recv(sk, skb);
		if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
			tcp_fin(sk);

		if (!RB_EMPTY_ROOT(&tp->out_of_order_queue)) {
			tcp_ofo_queue(sk);

			/* RFC5681. 4.2. SHOULD send immediate ACK, when
			 * gap in queue is filled.
			 */
			if (RB_EMPTY_ROOT(&tp->out_of_order_queue))
				inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_NOW;
		}

		if (tp->rx_opt.num_sacks)
			tcp_sack_remove(tp);

		tcp_fast_path_check(sk);

		if (eaten > 0)
			kfree_skb_partial(skb, fragstolen);
		if (!sock_flag(sk, SOCK_DEAD))
			tcp_data_ready(sk);
		return;
	}
    ......     

tcp_data_queu()使用tcp_queue_rcv()将数据加入到接收队列中。

//net/ipv4/tcp_input.c
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;
}

tcp_queue_rcv用于将接收到的skb加入到接收队列receive_queue()中,首先会调用tcp_try_coalesce()进行分段合并到队列中最后一个skb的尝试,若失败则调用__skb_queue_tail()添加该skb到队列尾部

并且最终tcp_data_queue()会调用tcp_data_ready()提醒当前sock有数据可读事件,源码如下:

//net/ipv4/tcp_input.c
void tcp_data_ready(struct sock *sk)
{
	const struct tcp_sock *tp = tcp_sk(sk);
	int avail = tp->rcv_nxt - tp->copied_seq;

	if (avail < sk->sk_rcvlowat && !sock_flag(sk, SOCK_DONE))
		return;

	sk->sk_data_ready(sk);
}

5.3.2 gdb验证

​ 断点设置

b tcp_v4_rcv
b tcp_v4_do_rcv
b tcp_rcv_established
b tcp_data_queue
b tcp_queue_rcv
b tcp_data_ready

​ 运行结果

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

5.4 应用层

函数从最开始的recv函数,进入系统调用__sys_recvfrom():

  1. recv函数,最终通过c库recv_from(),发起系统调用__sys_recvfrom()
  2. sys_recvfrom()中会创建一些需要的结构体,如struct msghdr。最终调用sock_recvmsg()。
  3. sock_recvmsg()先调用security_socket_recvmsg()进行安全性检查,然后再调用sock_recvmsg_nosec()函数接收数据。
  4. sock_recvmsg_nosec()函数会调用inet_recvmsg()
  5. inet_recvmsg()函数是间接调用了tcp_recvmsg或者udp_recvmsg,而这取决于传入的sock->sk->sk_prot中的类型,在本次实验中,socket初始化中,已经将sk_prot定义为tcp_prot,也就是说将会执行tcp_recvmsg()函数。

5.4.1 源码分析

从最开始应用程序调用 recv函数时,该函数会调用也是会调用系统调用__sys_recvfrom(),函数源码如下:

//net/socket.c
SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
		unsigned int, flags, struct sockaddr __user *, addr,
		int __user *, addr_len)
{
	return __sys_recvfrom(fd, ubuf, size, flags, addr, addr_len);
}

/*
 *	Receive a datagram from a socket.
 */

SYSCALL_DEFINE4(recv, int, fd, void __user *, ubuf, size_t, size,
		unsigned int, flags)
{
	return __sys_recvfrom(fd, ubuf, size, flags, NULL, NULL);
}
//net/socket.c
/*
 *	Receive a frame from the socket and optionally record the address of the
 *	sender. We verify the buffers are writable and if needed move the
 *	sender address from kernel to user space.
 */
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;
	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;
	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;
}

__sys_recvfrom()函数调用了sock_recvmsg()函数进行数据的接收,该函数源码如下:

//net/socket.c
int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags)
{
	int err = security_socket_recvmsg(sock, msg, msg_data_left(msg), flags);

	return err ?: sock_recvmsg_nosec(sock, msg, flags);
}

与send类似,首先先进行安全性检查,然后调用sock_recvmsg_nosec()获得数据sock_recvmsg_nosec()函数源码如下:

//net/socket.c
static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg,
				     int flags)
{
	return INDIRECT_CALL_INET(sock->ops->recvmsg, inet6_recvmsg,
				  inet_recvmsg, sock, msg, msg_data_left(msg),
				  flags);
}

通过继续分析可知这个函数会调用inet_recvmsg函数,该函数的内容如下:

//net/ipv4/af_inet.c
int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
		 int flags)
{
	struct sock *sk = sock->sk;
	int addr_len = 0;
	int err;

	if (likely(!(flags & MSG_ERRQUEUE)))
		sock_rps_record_flow(sk);

	err = INDIRECT_CALL_2(sk->sk_prot->recvmsg, tcp_recvmsg, udp_recvmsg,
			      sk, msg, size, flags & MSG_DONTWAIT,
			      flags & ~MSG_DONTWAIT, &addr_len);
	if (err >= 0)
		msg->msg_namelen = addr_len;
	return err;
}

//include/linux/indirect_call_wrapper.h
#define INDIRECT_CALL_1(f, f1, ...)					\
	({								\
		likely(f == f1) ? f1(__VA_ARGS__) : f(__VA_ARGS__);	\
	})
#define INDIRECT_CALL_2(f, f2, f1, ...)					\
	({								\
		likely(f == f2) ? f2(__VA_ARGS__) :			\
				  INDIRECT_CALL_1(f, f1, __VA_ARGS__);	\
	})

从上述代码和宏定义中可以发现,inet_recvmsg函数是间接调用了tcp_recvmsg或者udp_recvmsg,而这取决于传入的sock->sk->sk_prot中的类型,在本次实验中,socket初始化中,已经将sk_prot定义为tcp_prot,也就是说将会执行tcp_recvmsg()函数。

5.4.2 gdb验证

​ 断点设置

b __sys_recvfrom
b sock_recvmsg
b security_socket_recvmsg
b sock_recvmsg_nosec
b inet_recvmsg
b tcp_recvmsg

​ 运行结果

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

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

6. 完整时序图

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

上一篇:Packet fragmentation and segmentation offload in UDP and VXLAN


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