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

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

目录

目录

调研要求:

  • 在深入理解Linux内核任务调度(中断处理、softirg、tasklet、wq、内核线程等)机制的基础上,分析梳理send和recv过程中TCP/IP协议栈相关的运行任务实体及相互协作的时序分析。
  • 编译、部署、运行、测评、原理、源代码分析、跟踪调试等
  • 应该包括时序图

正文:

1.网络模型

分层的OSI模型和TCP/IP模型

​ 开放式系统互联通信参考模型(英语:Open System Interconnection Reference Model,缩写为 OSI),简称为OSI模型(OSI model),一种概念模型,由国际标准化组织提出,OSI/RM协议是由ISO(国际标准化组织)制定的,它有三个基本的功能:提供给开发者一个必须的、通用的概念以便开发完善、可以用来解释连接不同系统的框架。

OSI将计算机网络体系结构(architecture)划分为以下七层:

  • 物理层: 将数据转换为可通过物理介质传送的电子信号 相当于邮局中的搬运工人。
  • 数据链路层: 决定访问网络介质的方式。在此层将数据分帧,并处理流控制。本层指定拓扑结构并提供硬件寻址,相当于邮局中的装拆箱工人。
  • 网络层: 使用权数据路由经过大型网络 相当于邮局中的排序工人。
  • 传输层: 提供终端到终端的可靠连接 相当于公司中跑邮局的送信职员。
  • 会话层: 允许用户使用简单易记的名称建立连接 相当于公司中收寄信、写信封与拆信封的秘书。
  • 表示层: 协商数据交换格式 相当公司中简报老板、替老板写信的助理。
  • 应用层: 用户的应用程序和网络之间的接口。

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

​ 目前为止,大部分情况下都是使用TCP/IP四层模型,这一模型比起OSI七层模型更加简洁,适用。TCP/IP协议族是一个四层协议系统,自底而上分别是数据链路层、网络层、传输层和应用层。每一层完成不同的功能,且通过若干协议来实现,上层协议使用下层协议提供的服务。

​ TCP/IP参考模型分为四个层次:应用层、传输层、网络互连层和主机到网络层,如下图所示:

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

OSI模型和TCP/IP模型的对应关系如下:

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

linux中的网络协议栈

​ linux内核中的网络模型与上图的分层模型相似,Linux的协议栈其实是源于BSD的协议栈,它向上以及向下的接口以及协议栈本身的软件分层组织的非常好。

​ Linux的协议栈基于分层的设计思想,总共分为四层,从下往上依次是:物理层,链路层,网络层,应用层,物理层主要提供各种连接的物理设备,如各种网卡,串口卡等;链路层主要指的是提供对物理层进行访问的各种接口卡的驱动程序,如网卡驱动等;网路层的作用是负责将网络数据包传输到正确的位置,最重要的网络层协议当然就是IP协议了,其实网络层还有其他的协议如ICMP,ARP,RARP等,只不过不像IP那样被多数人所熟悉;传输层的作用主要是提供端到端,说白一点就是提供应用程序之间的通信,传输层最著名的协议非TCP与UDP协议末属了;应用层,顾名思义,当然就是由应用程序提供的,用来对传输数据进行语义解释的“人机界面”层了,比如HTTP,SMTP,FTP等等,其实应用层还不是人们最终所看到的那一层,最上面的一层应该是“解释层”,负责将数据以各种不同的表项形式最终呈献到人们眼前。

​   Linux网络协议栈结构Linux的整个网络协议栈都构建与Linux Kernel中,整个栈也是严格按照分层的思想来设计的,整个栈共分为五层,分别是 :

  1. 系统调用接口层,实质是一个面向用户空间应用程序的接口调用库,向用户空间应用程序提供使用网络服务的接口。
  2. 协议无关的接口层,就是SOCKET层,这一层的目的是屏蔽底层的不同协议(更准确的来说主要是TCP与UDP,当然还包括RAW IP, SCTP等),以便与系统调用层之间的接口可以简单,统一。简单的说,不管我们应用层使用什么协议,都要通过系统调用接口来建立一个SOCKET,这个SOCKET其实是一个巨大的sock结构,它和下面一层的网络协议层联系起来,屏蔽了不同的网络协议的不同,只吧数据部分呈献给应用层(通过系统调用接口来呈献)。
  3. 网络协议实现层,毫无疑问,这是整个协议栈的核心。这一层主要实现各种网络协议,最主要的当然是IP,ICMP,ARP,RARP,TCP,UDP等。这一层包含了很多设计的技巧与算法,相当的不错。
  4. 与具体设备无关的驱动接口层,这一层的目的主要是为了统一不同的接口卡的驱动程序与网络协议层的接口,它将各种不同的驱动程序的功能统一抽象为几个特殊的动作,如open,close,init等,这一层可以屏蔽底层不同的驱动程序。
  5. 驱动程序层,这一层的目的就很简单了,就是建立与硬件的接口层。

​ 可以看到,Linux网络协议栈是一个严格分层的结构,其中的每一层都执行相对独立的功能,结构非常清晰,而本文更加关注的是与协议无关的接口层(SOCKET层)和网络协议实现层。

2.SOCKET简介

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

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

	其实socket也没有层的概念,它只是一个facade设计模式的应用,让编程变的更简单,是一个软件抽象层。在网络编程中,我们也大部分使用的是socket编程。

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

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

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

socket编程中的函数简单介绍

只是大致介绍了一下socket用到的各个函数,具体参数的定义可以参考如下网址:https://www.cnblogs.com/wmx-learn/p/5312259.html

函数名 返回值 功能描述
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的第一个参数。

socket结构体源码

struct socket {
	socket_state		state;//套接字所处状态,比如CONNECTED
	short			type;//type是socket的类型,SOCK_DGRAM = 1,  SOCK_STREAM = 2,  SOCK_RAW    = 3,  SOCK_RDM    = 4,  SOCK_SEQPACKET = 5,  SOCK_DCCP   = 6,  SOCK_PACKET = 10,
	unsigned long		flags//标志位,负责一些特殊的设置,比如SOCK_ASYNC_NOSPACE
	struct file		*file;//与socket相关的指针列表
	struct sock		*sk;// sk是网络层对于socket的表示
	const struct proto_ops	*ops;//ops是协议相关的一组操作集,协议栈中总共定义了三个strcut proto_ops类型的变量,分别是myinet_stream_ops, myinet_dgram_ops, myinet_sockraw_ops,对应流协议, 数据报和原始套接口协议的操作函数集。
	struct socket_wq	wq;//等待队列
};

3.实验测试代码

服务端代码

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

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

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

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

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

#include <sys/time.h>

#include <sys/types.h>

#include <unistd.h>

#include <fcntl.h>

#include <sys/socket.h>

#include <errno.h>

#include <arpa/inet.h>

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



#define true 1

#define false 0



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

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



int main()

{

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

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

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

    unsigned int sin_size;


	//创建一个新的sokcet
    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 */


	//使用bind函数绑定
    if (bind(sockfd, (struct sockaddr *)&sa, sizeof(sa)) == -1)

    {

        perror("bind");

        exit(1);

    }


	//进行监听
    if (listen(sockfd, BACKLOG) == -1)

    {

        perror("listen");

        exit(1);

    }

    printf("Listenning");

    /* 主循环 */

    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)

        {

            

            char recvBuf[100];

            int len =0;

            
			//接受信息
            if((len=recv(new_fd, recvBuf, sizeof(recvBuf), 0))==-1){

                perror("recv");

                close(new_fd);

                exit(0);

            }

            recvBuf[len]='\0';

            printf("Received from %s %d: %s", inet_ntoa(their_addr.sin_addr),ntohs(their_addr.sin_port),recvBuf);

            if (send(new_fd, "Hi\n", 14, 0) == -1)

                perror("send");

            close(new_fd);

            exit(0);

        }

        close(new_fd);
        /*清除所有子进程 */

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

    }

    close(sockfd);

    return true;
}

客户端代码

#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 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("localhost")) == 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.s_addr = INADDR_ANY;

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



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

                sizeof(struct sockaddr)) == -1)

    {

        perror("connect");

        exit(1);

    }



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

        perror("send");

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

    {

        perror("recv");

        exit(1);

    }



    buf[numbytes] = '\0';

    printf("Received from %s %d: %s",inet_ntoa(server_addr.sin_addr),ntohs(server_addr.sin_port),buf);

    close(sockfd);



    return true;

}

4.调试环境

内核版本:linux-5.4.34

用Busybox构件简单的文件系统,用qemu加上gdv vmlinux 进行断点调试和源码查询

5.send过程分析

5.1 应用层分析

5.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);
}

​ 当socket创建好后,用户态调用send()函数进行数据发送,当调用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.
 *	向给定地址发送数据报。我们将地址移动到内核空间,并在调用协议之前检查用户空间数据区域是否可读。
 */
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;
    //定义一个msghdr来表示要发送数据的一些属性
	struct msghdr msg;
    //struct iovec,他被称为io向量,用来表示io数据的一些信息。
	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;
}

其中msghdr和iovec定义的结构体内容如下所示:

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. 要发送或者接收的iovec */
    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.  接收信息的flag*/
  };

/* Structure for scatter/gather I/O.  */
struct iovec
  {
    void *iov_base;	/* Pointer to data.要传输数据的用户态下的地址  */
    size_t iov_len;	/* Length of data. 要传输数据的长度 */
  };

所以,__sys_sendto函数其实做了3件事:

  1. 通过fd获取了对应的struct socket
  2. 创建了用来描述要发送的数据的结构体struct msghdr
  3. 调用了sock_sendmsg来执行实际的发送。

接下来我们分析这个sock_sendmsg

/**
 *  net/socket.c
 *	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.
 */
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()函数中,为了在发送多个消息时,提高性能会调用sock_sendmsg_nosec(),这是为了跳过上面的安全检查,提高性能。简单检查后就进入__sock_sendmsg_nosec()

//net/socket.c
//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指定的函数,该函数的内容如下:

//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()函数。

//net/ipv4/tcp_ipv4.c
struct proto tcp_prot = {
	....
	.sendmsg		= tcp_sendmsg,
	....
	}

5.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内核中的运行时序分析

​ 调试验证的结果如下图:

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

本次的调用堆栈也如下所示:

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

5.1.3 大致流程总结

​ 网络应用通过调用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初始化中完成赋值的。

5.2 传输层分析

5.2.1 源码过程分析

​ 在上层中最终调用了struct prot tcp_prot中的tcp_sendmsg()函数)进入运输层,我们先分析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;
}

可知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_sendmsg_locked中,完成的是将所有的数据组织成发送队列,这个发送队列是struct sock结构中的一个域sk_write_queue,这个队列的每一个元素是一个skb,里面存放的就是待发送的数据,tcp_sendmsg()只要是在处理用户数据的存放,优先考虑报文的线性区,然后是分页区,必要时需要使用新skb或者新分页来存放用户数据。然后调用了tcp_push()函数。

//include/net/sock.h
struct sock{
	...
	struct sk_buff_head	sk_write_queue;/*指向skb队列的第一个元素*/
	...
	struct sk_buff	*sk_send_head;/*指向队列第一个还没有发送的元素*/
}

接下来我们看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);
}

tcp_push()中涉及到小包阻塞的问题,使用了TSQ机制,即TCP Small Queue,通过tcp_should_autocork()判断是否开启。

static bool tcp_should_autocork(struct sock *sk, struct sk_buff *skb, int size_goal)
{
    return skb->len < size_goal && //数据报文长度小于最大报文值,也就是小包
           sysctl_tcp_autocorking && //默认开启,由/proc/sys/net/ipv4/tcp_autocorking控制
           skb != tcp_write_queue_head(sk) && //该报文不是第一个将要发送的报文,即还有其他数据等待发送
           //网卡队列中还有数据,sk_wmem_alloc的值在报文发送后会减小
           atomic_read(&sk->sk_wmem_alloc) > skb->truesize; 
}12345678

可以看出,其基本思想就是利用数据报文发送的这段时间,将小包尽量组合成大包发送,既减小发送带宽,同时也不会降低传输速率(网卡和发送队列有报文发送时才阻塞,无报文等待发送时,就直接发送,不阻塞)。如果没有阻塞,则调用__tcp_push_pending_frames()继续往下递交数据发送。

接下来查看__tcp_push_pending_frames()函数的源码,如下:

/* 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()实现了tcp的拥塞控制,然后调用了tcp_transmit_skb(sk, skb, 1, gfp)传输数据,实际上调用的是__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;
}

tcp_transmit_skb是tcp发送数据位于传输层的最后一步,这里首先对TCP数据段的头部进行了处理,然后调用了网络层提供的发送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl)实现了数据的发送,自此,数据离开了传输层,传输层的任务也就结束了。

5.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内核中的运行时序分析

​ 验证结果如图:

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

​ 函数调用堆栈如图所示:

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

5.2.3 大致流程总结

​ 在传输层中,第一个入口函数是上层调用的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)函数,进入网络层

5.3 网络层分析

5.3.1 源码过程分析

​ 由上层分析,可知传输层最后通过__tcp_transmit_skb()函数中的icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl)进入网络层,实现数据的发送。我们使用ip协议,ip_queue_xmit是ip层提供给tcp层的回调函数,我们查看这个函数的源码,可以发现这个函数就是调用了__ip_queue_xmit(sk, skb, fl, inet_sk(sk)->tos)

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

ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl,u8 tos)在路由结束,构造部分ip头部内容,调用ip_local_out(net, sk, skb)函数来发送数据,分析ip_local_out(net, sk, skb)函数源码,可以发现这个函数其实也就是调用了__ip_local_out(net, sk, skb)这个函数。

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

neigh_hh_output()函数最终通过dev_queue_xmit(skb)进入链路层

5.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内核中的运行时序分析

​ 验证结果如图:

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

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

​ 函数调用堆栈如图:

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

5.3.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()进入链路层

5.4 链路层分析

5.4.1 源码过程分析

​ 从网络层的dev_queue_xmit()进入链路层,而dev_queue_xmit()就是调用了__dev_queue_xmit()这个函数,源码如下:

//net/core/dev.c
int dev_queue_xmit_accel(struct sk_buff *skb, struct net_device *sb_dev)
{
	return __dev_queue_xmit(skb, sb_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_queue_xmit()函数的分析来看,发送报文有2中情况:

  1. 有拥塞控制策略的情况,比较复杂,但是目前最常用;
  2. 没有enqueue的状况,比较简单,直接发送到driver,如loopback等使用。

​ 先检查是否有enqueue的规则,如果有即调用__dev_xmit_skb进入拥塞控制的flow,如果没有且txq处于On的状态,那么就调用dev_hard_start_xmit()直接发送到driver。

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

dev_hard_start_xmit()函数通过循环调用xmit_one()函数发送一个或者多个数据,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()函数,其目的就是将封包送到driver的tx函数。

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

​ 最后__netdev_start_xmit()调用为网卡编写的驱动中的回调函数指针,从而把数据发送给网卡,物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中。在数据拷贝中,同时加入符合以太网协议的相关header,IFG、前导符和CRC。对于以太网网络,物理层发送采用CSMA/CD,即在发送过程中侦听链路冲突。一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。

5.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内核中的运行时序分析

​ 验证结果如图:

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

​ 函数调用堆栈如图:

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

5.4.3 大致流程总结

​ 从网络层的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()函数发送一个或者多个数据。
  4. xmit_one()会调用 netdev_start_xmit()
  5. netdev_start_xmit()实际调用的是__netdev_start_xmit()函数,其目的就是将封包送到driver的tx函数。
  6. __netdev_start_xmit()会调用为网卡编写的驱动中的回调函数指针,从而把数据发送给网卡。

5.5 物理层分析

​ 通过__netdev_start_xmit()调用网卡驱动中的相应函数ndo_start_xmit()进行执行,大致流程从网上博客看到如下:

  1. 将skb放入网卡自己的发送队列
  2. 通知网卡发送数据包
  3. 网卡发送完成后发送中断给CPU
  4. 收到中断后进行skb的清理工作

6.recv过程分析

6.1 物理层分析

​ 由于一开始在数据包到达网卡时,内核和网络设备驱动是通过中断的方式来处理的。因此对于recv的分析,我打算先从物理层开始,慢慢往上递进。

​ 首先需要知道的是有时候一个中断处理过程太过复杂和耗时,因此linux中的中断处理分为上半部分和下半部分,由于简单来说是上半部分处理的事务是紧急的,即它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作。“登记中断”意味着将下半部分处理程序挂到该设备的下半部分执行队列中去。这样,上半部分执行的速度就会很快,可以服务更多的中断请求。下半部分几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是下半部分和上半部分的最大不同,因为上半部分往往被设计成不可中断。下半部分则相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。

​ 当设备上有数据到达的时候,会给CPU的相关引脚上触发一个电压变化,以通知CPU来处理数据。上半部是只进行最简单的工作,快速处理然后释放CPU,接着CPU就可以允许其它中断进来。剩下将绝大部分的工作都放到下半部中,可以慢慢从容处理。2.4以后的内核版本采用的下半部实现方式是软中断,由ksoftirqd内核线程全权处理。和硬中断不同的是,硬中断是通过给CPU物理引脚施加电压变化,而软中断是通过给内存中的一个变量的二进制值以通知软中断处理程序。

​ 简单来说就是,当网卡上收到数据以后,网络驱动会以DMA的方式把网卡上收到的帧写到内存里。再向CPU发起一个中断,以通知CPU有数据到达。然后当CPU收到中断请求后,会去调用网络驱动注册的中断处理函数。网卡的中断处理函数并不做过多工作,发出软中断请求,然后尽快释放CPU。ksoftirqd检测到有软中断请求到达,调用poll开始轮询收包,收到后交由各级协议栈处理。

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

​ 接下来粗略地看一下硬中断处理和ksoftirqd内核线程处理软中断:

6.1.1 硬中断处理

​ 首先当数据帧从网线到达网卡上的时候,第一站是网卡的接收队列。网卡在分配给自己的RingBuffer中寻找可用的内存位置,找到后DMA引擎会把数据DMA到网卡之前关联的内存里,这个时候CPU都是无感的。当DMA操作完成以后,网卡会像CPU发起一个硬中断,通知CPU有数据到达。

​ 在硬中断的执行最后会发起一个软中断NET_RX_SOFTIRQ,然后进入软中断过程。

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

6.1.2 软中断过程

​ 首先在内核启动过程中,会为内核注册一些软中断处理函数,linux内核通过调用subsys_initcall()来初始化各个子系统,在网络子系统的初始化,会执行到net_dev_init()函数,然后net_dev_init()函数会为相应的软中断注册处理函数,流程如下图所示。

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

//net/core/dev.c
subsys_initcall(net_dev_init);

//net/core/dev.c
/*
 *	Initialize the DEV module. At boot time this walks the device list and
 *	unhooks any devices that fail to initialise (normally hardware not
 *	present) and leaves us with a valid list of present and active devices.
 *
 */

/*
 *       This is called single threaded during boot, so no need
 *       to take the rtnl semaphore.
 */
static int __init net_dev_init(void)
{
	int i, rc = -ENOMEM;

	BUG_ON(!dev_boot_phase);

	if (dev_proc_init())
		goto out;

	if (netdev_kobject_init())
		goto out;

	INIT_LIST_HEAD(&ptype_all);
	for (i = 0; i < PTYPE_HASH_SIZE; i++)
		INIT_LIST_HEAD(&ptype_base[i]);

	INIT_LIST_HEAD(&offload_base);

	if (register_pernet_subsys(&netdev_net_ops))
		goto out;

	/*
	 *	Initialise the packet receive queues.
	 */

	for_each_possible_cpu(i) {
		struct work_struct *flush = per_cpu_ptr(&flush_works, i);
		struct softnet_data *sd = &per_cpu(softnet_data, i);

		INIT_WORK(flush, flush_backlog);

		skb_queue_head_init(&sd->input_pkt_queue);
		skb_queue_head_init(&sd->process_queue);
#ifdef CONFIG_XFRM_OFFLOAD
		skb_queue_head_init(&sd->xfrm_backlog);
#endif
		INIT_LIST_HEAD(&sd->poll_list);
		sd->output_queue_tailp = &sd->output_queue;
#ifdef CONFIG_RPS
		sd->csd.func = rps_trigger_softirq;
		sd->csd.info = sd;
		sd->cpu = i;
#endif

		init_gro_hash(&sd->backlog);
		sd->backlog.poll = process_backlog;
		sd->backlog.weight = weight_p;
	}

	dev_boot_phase = 0;

	/* The loopback device is special if any other network devices
	 * is present in a network namespace the loopback device must
	 * be present. Since we now dynamically allocate and free the
	 * loopback device ensure this invariant is maintained by
	 * keeping the loopback device as the first device on the
	 * list of network devices.  Ensuring the loopback devices
	 * is the first device that appears and the last network device
	 * that disappears.
	 */
	if (register_pernet_device(&loopback_net_ops))
		goto out;

	if (register_pernet_device(&default_device_ops))
		goto out;

	open_softirq(NET_TX_SOFTIRQ, net_tx_action);
	open_softirq(NET_RX_SOFTIRQ, net_rx_action);

	rc = cpuhp_setup_state_nocalls(CPUHP_NET_DEV_DEAD, "net/dev:dead",
				       NULL, dev_cpu_dead);
	WARN_ON(rc < 0);
	rc = 0;
out:
	return rc;
}

​ 可以发现在net_dev_init()函数中,通过open_softirq()为NET_TX_SOFTIRQ和NET_RX_SOFTIRQ注册了相应的中断处理函数,继续跟踪open_softirq后发现这个注册的方式是记录在softirq_vec变量里的。后面ksoftirqd线程收到软中断的时候,也会使用这个变量来找到每一种软中断对应的处理函数。

//kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}

​ 上述已知,内核已经为NET_RX_SOFTIRQ注册了处理函数net_rx_action(),接下来我们分析软中断执行过程。

​ 首先Linux的软中断都是在专门的内核线程(ksoftirqd)中进行的,当ksoftirqd被创建出来以后,它就会进入自己的线程循环函数ksoftirqd_should_runrun_ksoftirqd了。不停地判断有没有软中断需要被处理,源码如下所示:

//kernel/softirq.c
static struct smp_hotplug_thread softirq_threads = {
	.store			= &ksoftirqd,
	.thread_should_run	= ksoftirqd_should_run,
	.thread_fn		= run_ksoftirqd,
	.thread_comm		= "ksoftirqd/%u",
};

//include/linux/interrupt.h
#define local_softirq_pending()	(__this_cpu_read(local_softirq_pending_ref))

//kernel/softirq.c
static int ksoftirqd_should_run(unsigned int cpu){
    return local_softirq_pending();
}


//kernel/softirq.c
static void run_ksoftirqd(unsigned int cpu){
    local_irq_disable();
    if (local_softirq_pending()) {
        __do_softirq();
        rcu_note_context_switch(cpu);
        local_irq_enable();
        cond_resched();
        return;
    }
    local_irq_enable();

}

​ 可以看到ksoftirqd_should_run()只是读取了中断位,真正的线程函数是run_ksoftirqd(),run_ksoftirqd()函数中执行了__do_softirq()函数,该函数判断根据当前CPU的软中断类型,调用其注册的action方法,已经知道内核为NET_RX_SOFTIRQ注册了处理函数net_rx_action(),也就是说最终会执行到net_rx_action()这个函数。

6.2 链路层分析

6.2.1 源码过程分析

​ 上层说到ksoftirqd处理软中断,通知内核接收到新的数据帧是通过net_rx_action函数,接下来我们看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函数执行软中断NET_RX_SOFTIRQ时会遍历poll_list链表,然后调用网卡驱动注册到的napi_poll(),在网卡驱动的napi_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
/**
 *	netif_receive_skb_core - special purpose version of netif_receive_skb
 *	@skb: buffer to process
 *
 *	More direct receive version of netif_receive_skb().  It should
 *	only be used by callers that have a need to skip RPS and Generic XDP.
 *	Caller must also take care of handling if (page_is_)pfmemalloc.
 *
 *	This function may only be called from softirq context and interrupts
 *	should be enabled.
 *
 *	Return values (usually ignored):
 *	NET_RX_SUCCESS: no congestion
 *	NET_RX_DROP: packet was dropped
 */
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_one_core()函数。

//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;
}
//这里与send过程中分析的隐式调用一样,会调用相应的func函数即ip_rcv()

__netif_receive_skb_one_core()函数会将数据包交给上层ip_rcv()进行处理。

6.2.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内核中的运行时序分析

​ 验证结果如图:

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

​ 函数调用堆栈如图:

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

​ 由调用栈可知,recv确实是执行了__do_softirq(),从软中断往上传递的。

6.2.3 大致流程总结

​ 从中断上半部开始到软中断,执行到了软中断处理函数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()进行处理。

6.3 网络层分析

6.3.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);
}

​ 可知这里也首先会进入hook点NF_INET_PRE_ROUTING进行处理,然后通过后再调用ip_rcv_finish()函数

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

ip_rcv_finish()主要工作是完成路由表的查询,决定报文经过IP层处理后,是继续向上传递,还是进行转发,还是丢弃。最终,没有被丢弃的(即为本机需要的包)会进入到dst_input(),该函数的源码如下:

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

skb_dst(skb)->input调用的input方法就是路由子系统赋的ip_local_deliver(),函数源码如下:

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

​ 可以发现在路由结束后,首先先判断是否需要有分片,有的话先整合分片,之后再传入本机的要需要再经过一个HOOK点,NF_INET_LOCAL_IN。通过之后执行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_local_deliver_finish()函数执行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);
		}
	}
}

​ 同样地用到了INDIRECT_CALL_2这个隐式调用的宏,这里将会根据包中的协议类型选择进行分发,在这里skb包将会进一步被派送到更上层的协议中,udp和tcp,本次实验是调用了tcp_v4_rcv()这个函数,进入到传输层。

6.3.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内核中的运行时序分析
​ 验证结果如图:

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

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

​ 函数调用堆栈如图:

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

6.3.3 大致流程总结

​ 从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()

6.4 传输层分析

6.4.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
/*
 *	TCP receive function for the ESTABLISHED state.
 *
 *	It is split into a fast path and a slow path. The fast path is
 * 	disabled when:
 *	- A zero window was announced from us - zero window probing
 *        is only handled properly in the slow path.
 *	- Out of order segments arrived.
 *	- Urgent data is expected.
 *	- There is no buffer space left
 *	- Unexpected TCP flags/window values/header lengths are received
 *	  (detected by checking the TCP header against pred_flags)
 *	- Data is sent in both directions. Fast path only supports pure senders
 *	  or pure receivers (this means either the sequence number or the ack
 *	  value must stay constant)
 *	- Unexpected TCP option.
 *
 *	When these conditions are not satisfied it drops into a standard
 *	receive procedure patterned after RFC793 to handle all cases.
 *	The first three cases are guaranteed by proper pred_flags setting,
 *	the rest is checked inline. Fast processing is turned on in
 *	tcp_data_queue when everything is OK.
 */
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用于处理已连接状态下的输入,处理过程根据首部预测字段分为快速路径和慢速路径。

  • 快速路径:用于处理预期的,理想情况下的数据段,在这种情况下,不会对一些边缘情形进行检测,进而达到快速处理的目的;
    • 无数据的情况下:若无数据,则处理输入ack,释放该skb,检查是否有数据发送,有则发送;
    • 有数据的情况下:有数据,则使用tcp_queue_rcv()将数据加入到接收队列中。
  • 慢速路径:用于处理那些非预期的,非理想情况下的数据段,即不满足快速路径的情况下数据段的处理;这种情况下会进行更详细的校验,然后处理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);
}

​ 接下来我们就需要从从应用层看向传输层,因此读者可以先看下一章的内容,再返回来,这里为了完整性,直接写下去:

​ 从应用层函数tcp_recvmsg()进入,我们先看tcp_recvmsg()的源码:

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

​ 这里共维护了三个队列:prequeuebacklogreceive_queue,分别为预处理队列,后备队列和接收队列,在连接建立后,若没有数据到来,接收队列为空,进程会在sk_busy_loop()函数内循环等待,直到接收队列不为空。

​ 若接收队列不为空,并调用函数数skb_copy_datagram_msg将接收到的数据拷贝到用户态。

//include/linux/skbuff.h
static inline int skb_copy_datagram_msg(const struct sk_buff *from, int offset,
					struct msghdr *msg, int size)
{
	return skb_copy_datagram_iter(from, offset, &msg->msg_iter, size);
}

​ 可知实际上调用了skb_copy_datagram_iter

//net/core/datagram.c
/**
 *	skb_copy_datagram_iter - Copy a datagram to an iovec iterator.
 *	@skb: buffer to copy
 *	@offset: offset in the buffer to start copying from
 *	@to: iovec iterator to copy to
 *	@len: amount of data to copy from buffer to iovec
 */
int skb_copy_datagram_iter(const struct sk_buff *skb, int offset,
			   struct iov_iter *to, int len)
{
	trace_skb_copy_datagram_iovec(skb, len);
	return __skb_datagram_iter(skb, offset, to, len, false,
			simple_copy_to_iter, NULL);
}

skb_copy_datagram_iter实际上是调用了__skb_datagram_iter

//net/core/datagram.c
static 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;

	/* Copy header. */
	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;
	}

	/* Copy paged appendix. Hmm... why does this look so complicated? */
	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 + skb_frag_off(frag) + offset - start,
			       copy, data, to);
			kunmap(page);
			offset += n;
			if (n != copy)
				goto short_copy;
			if (!(len -= copy))
				return 0;
		}
		start = end;
	}

	skb_walk_frags(skb, frag_iter) {
		int end;

		WARN_ON(start > offset + len);

		end = start + frag_iter->len;
		if ((copy = end - offset) > 0) {
			if (copy > len)
				copy = len;
			if (__skb_datagram_iter(frag_iter, offset - start,
						to, copy, fault_short, cb, data))
				goto fault;
			if ((len -= copy) == 0)
				return 0;
			offset += copy;
		}
		start = end;
	}
	if (!len)
		return 0;

	/* This is not really a user copy fault, but rather someone
	 * gave us a bogus length on the skb.  We should probably
	 * print a warning here as it may indicate a kernel bug.
	 */

fault:
	iov_iter_revert(to, offset - start_off);
	return -EFAULT;

short_copy:
	if (fault_short || iov_iter_count(to))
		goto fault;

	return 0;
}

​ 数据拷贝完后,再进行一些判断:

		if (copied >= target) {
			/* Do not sleep, just process backlog. */
			release_sock(sk);
			lock_sock(sk);
		} else {
			sk_wait_data(sk, &timeo, last);
		}

​ 比如此处判断是否拥有足够的数据,否则等待下一层传递上来。

6.4.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内核中的运行时序分析

​ 验证结果如图:

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

​ 函数调用堆栈如图:

TCP/IP协议栈在Linux内核中的运行时序分析
​ 从应用层往下读取:

​ 增加断点的代码如下:

b tcp_recvmsg
b skb_copy_datagram_msg
b skb_copy_datagram_iter
b __skb_datagram_iter

TCP/IP协议栈在Linux内核中的运行时序分析
​ 验证结果如图:

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

​ 函数调用堆栈如图:
TCP/IP协议栈在Linux内核中的运行时序分析

6.4.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有数据可读事件。

从应用层向下的大致流程如下

  1. 从应用层tcp_recvmsg()函数进入。
  2. tcp_recvmsg()函数共维护了三个队列:prequeuebacklogreceive_queue
    1. 若接收队列为空,进程会在sk_busy_loop()函数内循环等待,直到接收队列不为空。
    2. 一切就绪的情况下,会调用skb_copy_datagram_msg()->skb_copy_datagram_iter->__skb_datagram_iter进行拷贝消息。
  3. 加入队列为空,或者数据不足的情况下,则调用sk_wait_data()进行等待。

6.5 应用层分析

6.5.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
/**
 *	sock_recvmsg - receive a message from @sock
 *	@sock: socket
 *	@msg: message to receive
 *	@flags: message flags
 *
 *	Receives @msg from @sock, passing through LSM. Returns the total number
 *	of bytes received, or an error.
 */
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()函数。

6.5.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内核中的运行时序分析

​ 函数调用堆栈如图:

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

6.5.3 大致流程总结

​ 函数从最开始的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()函数。

7.时序图

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

8.参考文献

https://www.jianshu.com/p/f32cfd6c208b
http://blog.chinaunix.net/uid-21768364-id-2936798.html
https://www.cnblogs.com/wmx-learn/p/5312259.html
https://www.cnblogs.com/myguaiguai/p/12069485.html
https://blog.csdn.net/wenqian1991/article/details/46898725
https://www.cnblogs.com/kongwy/p/14328278.html
https://blog.csdn.net/u010039418/article/details/82020078
https://blog.csdn.net/wenqian1991/article/details/46725105
https://mp.weixin.qq.com/s/GoYDsfy9m0wRoXi_NCfCmg

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


下一篇:不为人知的网络编程(十):深入操作系统,从内核理解网络包的接收过程(Linux篇)