TCP/IP协议栈在Linux内核中的运行时序分析
- 网络程序设计调研报告
TCP/IP协议栈在Linux内核中的运行时序分析
姓名:周涛
学号:SA20225639
班级:大数据3班
2021年1月
调研要求
- 在深入理解Linux内核任务调度(中断处理、softirg、tasklet、wq、内核线程等)机制的基础上,分析梳理send和recv过程中TCP/IP协议栈相关的运行任务实体及相互协作的时序分析。
- 编译、部署、运行、测评、原理、源代码分析、跟踪调试等
- 应该包括时序
目录
1概述
1.1 Linux操作系统架构简介
1.2 网络模型
1.3 Linux内核协议栈
2 实验测试
2.1 调试环境
2.2 服务端代码
2.3 客户端代码
3 应用层流程
3.1 send和recv函数
3.2 发送端
3.3 接收端
4 传输层流程
4.1 发送端
4.2 接收端
5 IP层流程
5.1 发送端
5.2 接收端
6 网络层流程
5.1 发送端
5.2 接收端
7 时序图展示和总结
8 参考资料
1 概述
1.1 Linux操作系统架构简介
Linux操作系统总体上由Linux内核和GNU系统构成,具体来讲由4个主要部分构成,即Linux内核、Shell、文件系统和应用程序。内核、Shell和文件系统构成了操作系统的基本结构,使得用户可以运行程序、管理文件并使用系统。
内核是操作系统的核心,具有很多最基本功能,如虚拟内存、多任务、共享库、需求加载、可执行程序和TCP/IP网络功能。我们所调研的工作,就是在Linux内核层面进行分析。

1.2 网络模型
开放式系统互联通信参考模型(英语:Open System Interconnection Reference Model,缩写为 OSI),简称为OSI模型(OSI model),一种概念模型,由国际标准化组织提出,OSI/RM协议是由ISO(国际标准化组织)制定的,它有三个基本的功能:提供给开发者一个必须的、通用的概念以便开发完善、可以用来解释连接不同系统的框架。
OSI将计算机网络体系结构(architecture)划分为以下七层:
- 物理层: 将数据转换为可通过物理介质传送的电子信号 相当于邮局中的搬运工人。
- 数据链路层: 决定访问网络介质的方式。在此层将数据分帧,并处理流控制。本层指定拓扑结构并提供硬件寻址,相当于邮局中的装拆箱工人。
- 网络层: 使用权数据路由经过大型网络 相当于邮局中的排序工人。
- 传输层: 提供终端到终端的可靠连接 相当于公司中跑邮局的送信职员。
- 会话层: 允许用户使用简单易记的名称建立连接 相当于公司中收寄信、写信封与拆信封的秘书。
- 表示层: 协商数据交换格式 相当公司中简报老板、替老板写信的助理。
- 应用层: 用户的应用程序和网络之间的接口。

然而,实际日常生活中,大部分情况下使用的是TCP/IP模型,这一模型比起OSI更加简洁。TCP/IP协议族是一个四层协议系统,自底而上分别是数据链路层、网络层、传输层和应用层。每一层完成不同的功能,且通过若干协议来实现,上层协议使用下层协议提供的服务,如下图所示。

1.3 Linux内核协议栈
Linux的协议栈其实是源于BSD的协议栈,它向上以及向下的接口以及协议栈本身的软件分层组织的非常好。
Linux的协议栈基于分层的设计思想,总共分为四层,从下往上依次是:物理层,链路层,网络层,应用层。
物理层主要提供各种连接的物理设备,如各种网卡,串口卡等;链路层主要指的是提供对物理层进行访问的各种接口卡的驱动程序,如网卡驱动等;网路层的作用是负责将网络数据包传输到正确的位置,最重要的网络层协议当然就是IP协议了,其实网络层还有其他的协议如ICMP,ARP,RARP等,只不过不像IP那样被多数人所熟悉;传输层的作用主要是提供端到端,说白一点就是提供应用程序之间的通信,传输层最著名的协议非TCP与UDP协议末属了;应用层,顾名思义,当然就是由应用程序提供的,用来对传输数据进行语义解释的“人机界面”层了,比如HTTP,SMTP,FTP等等,其实应用层还不是人们最终所看到的那一层,最上面的一层应该是“解释层”,负责将数据以各种不同的表项形式最终呈献到人们眼前。
Linux网络核心架构Linux的网络架构从上往下可以分为三层,分别是:
用户空间的应用层。
内核空间的网络协议栈层。
物理硬件层。
其中最重要最核心的当然是内核空间的协议栈层了。
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网络协议栈是一个严格分层的结构,其中的每一层都执行相对独立的功能,结构非常清晰。
2 实验测试
2.1 调试环境
内核版本:linux-5.4.34
用Busybox构件简单的文件系统,用qemu加上gdv vmlinux 进行断点调试和源码查询
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 MYPORT 3490 /* 监听的端口 */ #define BACKLOG 10 /* listen的请求接收队列长度 */ #define BUF_SIZE 1024 int main() { int sockfd; if ((sockfd = socket(PF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } struct sockaddr_in sa; /* 自身的地址信息 */ 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); } struct sockaddr_in their_addr; /* 连接对方的地址信息 */ unsigned int sin_size = 0; char buf[BUF_SIZE]; int ret_size = recvfrom(sockfd, buf, BUF_SIZE, 0, (struct sockaddr *)&their_addr, &sin_size); if(ret_size == -1) { perror("recvfrom"); exit(1); } buf[ret_size] = '\0'; printf("recvfrom:%s", buf); }
2.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 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_DGRAM, 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 ((numbytes = sendto(sockfd, "Hello, world!\n", 14, 0, (struct sockaddr *)&server_addr, sizeof(server_addr))) == -1) { perror("sendto"); exit(1); } close(sockfd); return true; }
3 应用层流程
3.1 send和recv函数
函数原型
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags); ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
(1) send解析
sockfd:指定发送端套接字描述符。
buff: 存放要发送数据的缓冲区
nbytes: 实际要改善的数据的字节数
flags: 一般设置为0
-
send先比较发送数据的长度nbytes和套接字sockfd的发送缓冲区的长度,如果nbytes > 套接字sockfd的发送缓冲区的长度, 该函数返回SOCKET_ERROR;
-
如果nbtyes <= 套接字sockfd的发送缓冲区的长度,那么send先检查协议是否正在发送sockfd的发送缓冲区中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送sockfd的发送缓冲区中的数据或者sockfd的发送缓冲区中没有数据,那么send就比较sockfd的发送缓冲区的剩余空间和nbytes
-
如果 nbytes > 套接字sockfd的发送缓冲区剩余空间的长度,send就一起等待协议把套接字sockfd的发送缓冲区中的数据发送完
-
如果 nbytes < 套接字sockfd的发送缓冲区剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把套接字sockfd的发送缓冲区中的数据传到连接的另一端的,而是协议传送的,send仅仅是把buf中的数据copy到套接字sockfd的发送缓冲区的剩余空间里)。
-
如果send函数copy成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR; 如果在等待协议传送数据时网络断开,send函数也返回SOCKET_ERROR。
-
send函数把buff中的数据成功copy到sockfd的改善缓冲区的剩余空间后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR。(每一个除send的socket函数在执行的最开始总要先等待套接字的发送缓冲区中的数据被协议传递完毕才能继续,如果在等待时出现网络错误那么该socket函数就返回SOCKET_ERROR)
-
在unix系统下,如果send在等待协议传送数据时网络断开,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的处理是进程终止。
(2) recv函数
sockfd: 接收端套接字描述符
buff: 用来存放recv函数接收到的数据的缓冲区
nbytes: 指明buff的长度
flags: 一般置为0
-
recv先等待s的发送缓冲区的数据被协议传送完毕,如果协议在传送sock的发送缓冲区中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR
-
如果套接字sockfd的发送缓冲区中没有数据或者数据被协议成功发送完毕后,recv先检查套接字sockfd的接收缓冲区,如果sockfd的接收缓冲区中没有数据或者协议正在接收数据,那么recv就一起等待,直到把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲区中的数据copy到buff中(注意协议接收到的数据可能大于buff的长度,所以在这种情况下要调用几次recv函数才能把sockfd的接收缓冲区中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的)
-
recv函数返回其实际copy的字节数,如果recv在copy时出错,那么它返回SOCKET_ERROR。如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
-
在unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用 recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

3.2 发送端
- 网络应用调用Socket API socket (int family, int type, int protocol) 创建一个 socket,该调用最终会调用 Linux system call socket() ,并最终调用 Linux Kernel 的 sock_create() 方法。该方法返回被创建好了的那个 socket 的 file descriptor。对于每一个 userspace 网络应用创建的 socket,在内核中都有一个对应的 struct socket和 struct sock。其中,struct sock 有三个队列(queue),分别是 rx , tx 和 err,在 sock 结构被初始化的时候,这些缓冲队列也被初始化完成;在收据收发过程中,每个 queue 中保存要发送或者接受的每个 packet 对应的 Linux 网络栈 sk_buffer 数据结构的实例 skb。
- 对于 TCP socket 来说,应用调用 connect()API ,使得客户端和服务器端通过该 socket 建立一个虚拟连接。在此过程中,TCP 协议栈通过三次握手会建立 TCP 连接。默认地,该 API 会等到 TCP 握手完成连接建立后才返回。在建立连接的过程中的一个重要步骤是,确定双方使用的 Maxium Segemet Size (MSS)。因为 UDP 是面向无连接的协议,因此它是不需要该步骤的。
- 应用调用 Linux Socket 的 send 或者 write API 来发出一个 message 给接收端
- sock_sendmsg 被调用,它使用 socket descriptor 获取 sock struct,创建 message header 和 socket control message
- _sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数。
- 对于 TCP ,调用 tcp_sendmsg 函数。
- 对于 UDP 来说,userspace 应用可以调用 send()/sendto()/sendmsg() 三个 system call 中的任意一个来发送 UDP message,它们最终都会调用内核中的 udp_sendmsg() 函数。

下面我们具体结合Linux内核源码进行一步步仔细分析:
根据上述分析可知,发送端首先创建socket,创建之后会通过send发送数据。具体到源码级别,会通过send,sendto,sendmsg这些系统调用来发送数据,而上述三个函数底层都调用了sock_sendmsg。见下图:

__sys_sendto函数:

__sys_sendto函数创建了两个结构体

sock_sendmsg函数:

sock_sendmsg_nosec函数:

inet_sendmsg函数:

至此,发送端调用完毕。我们可以通过gdb进行调试验证:

3.3 接收端
- 每当用户应用调用 read 或者 recvfrom 时,该调用会被映射为/net/socket.c 中的 sys_recv 系统调用,并被转化为 sys_recvfrom 调用,然后调用 sock_recgmsg 函数。
- 对于 INET 类型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法会被调用,它会调用相关协议的数据接收方法。
- 对 TCP 来说,调用 tcp_recvmsg。该函数从 socket buffer 中拷贝数据到 user buffer。
- 对 UDP 来说,从 user space 中可以调用三个 system call recv()/recvfrom()/recvmsg() 中的任意一个来接收 UDP package,这些系统调用最终都会调用内核中的 udp_recvmsg 方法。

接收端调用的是__sys_recvfrom函数:

sock_recvmsg函数:

sock_recvmsg_nose函数:
inet_recvmsg函数:

至此接收端调用分析完毕,下面用gdb打断点进行验证:


4 传输层流程
4.1 发送端
传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,主要功能包括 (1)构造 TCP segment (2)计算 checksum (3)发送回复(ACK)包 (4)滑动窗口(sliding windown)等保证可靠性的操作。TCP 协议栈的大致处理过程如下图所示:

传输层也是先调用send()->sendto()->sys_sento->sock_sendmsg->sock_sendmsg_nosec,sock_sendmsg_nosec这个函数:

在应用层调用的是inet_sendmsg函数,在传输层根据后面的断点可以知道,调用的是sock->ops-sendmsg这个函数。而sendmsg为一个宏,调用的是tcp_sendmsg,如下;
struct proto tcp_prot = { .name = "TCP", .owner = THIS_MODULE, .close = tcp_close, .pre_connect = tcp_v4_pre_connect, .connect = tcp_v4_connect, .disconnect = tcp_disconnect, .accept = inet_csk_accept, .ioctl = tcp_ioctl, .init = tcp_v4_init_sock, .destroy = tcp_v4_destroy_sock, .shutdown = tcp_shutdown, .setsockopt = tcp_setsockopt, .getsockopt = tcp_getsockopt, .keepalive = tcp_set_keepalive, .recvmsg = tcp_recvmsg, .sendmsg = tcp_sendmsg, ......
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size) { struct tcp_sock *tp = tcp_sk(sk);/*进行了强制类型转换*/ struct sk_buff *skb; flags = msg->msg_flags; ...... if (copied) tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH, size_goal); }
在tcp协议的头部有几个标志字段:URG、ACK、RSH、RST、SYN、FIN,tcp_push中会判断这个skb的元素是否需要push,如果需要就将tcp头部字段的push置一,置一的过程如下:
static void tcp_push(struct sock *sk, int flags, int mss_now, int nonagle, int size_goal) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; skb = tcp_write_queue_tail(sk); if (!skb) return; if (!(flags & MSG_MORE) || forced_push(tp)) tcp_mark_push(tp, skb); tcp_mark_urg(tp, flags); if (tcp_should_autocork(sk, skb, size_goal)) { /* avoid atomic op if TSQ_THROTTLED bit is already set */ if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) { NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING); set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags); } /* It is possible TX completion already happened * before we set TSQ_THROTTLED. */ if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize) return; } if (flags & MSG_MORE) nonagle = TCP_NAGLE_CORK; __tcp_push_pending_frames(sk, mss_now, nonagle); }
首先struct tcp_skb_cb结构体存放的就是tcp的头部,头部的控制位为tcp_flags,通过tcp_mark_push会将skb中的cb,也就是48个字节的数组,类型转换为struct tcp_skb_cb,这样位于skb的cb就成了tcp的头部。tcp_mark_push如下:
static inline void tcp_mark_push(struct tcp_sock *tp, struct sk_buff *skb) { TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH; tp->pushed_seq = tp->write_seq; } ... #define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0])) ... struct sk_buff { ... char cb[48] __aligned(8); ...
struct tcp_skb_cb { __u32 seq; /* Starting sequence number */ __u32 end_seq; /* SEQ + FIN + SYN + datalen */ __u8 tcp_flags; /* tcp头部标志,位于第13个字节tcp[13]) */ ...... };
然后,tcp_push调用了__tcp_push_pending_frames(sk, mss_now, nonagle);函数发送数据:
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle) { if (tcp_write_xmit(sk, cur_mss, nonagle, 0, sk_gfp_mask(sk, GFP_ATOMIC))) tcp_check_probe_timer(sk); }
发现它调用了tcp_write_xmit函数来发送数据:
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; unsigned int tso_segs, sent_pkts; int cwnd_quota; int result; bool is_cwnd_limited = false, is_rwnd_limited = false; u32 max_segs; /*统计已发送的报文总数*/ sent_pkts = 0; ...... /*若发送队列未满,则准备发送报文*/ while ((skb = tcp_send_head(sk))) { unsigned int limit; if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) { /* "skb_mstamp_ns" is used as a start point for the retransmit timer */ skb->skb_mstamp_ns = tp->tcp_wstamp_ns = tp->tcp_clock_cache; list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue); tcp_init_tso_segs(skb, mss_now); goto repair; /* Skip network transmission */ } if (tcp_pacing_check(sk)) break; tso_segs = tcp_init_tso_segs(skb, mss_now); BUG_ON(!tso_segs); /*检查发送窗口的大小*/ cwnd_quota = tcp_cwnd_test(tp, skb); if (!cwnd_quota) { if (push_one == 2) /* Force out a loss probe pkt. */ cwnd_quota = 1; else break; } if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) { is_rwnd_limited = true; break; ...... limit = mss_now; if (tso_segs > 1 && !tcp_urg_mode(tp)) limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle); if (skb->len > limit && unlikely(tso_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE, skb, limit, mss_now, gfp))) break; if (tcp_small_queue_check(sk, skb, 0)) break; if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) break; ......
tcp_write_xmit位于tcpoutput.c中,它实现了tcp的拥塞控制,然后调用了tcp_transmit_skb(sk, skb, 1, gfp)传输数据,实际上调用的是__tcp_transmit_skb:
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask, u32 rcv_nxt) { skb_push(skb, tcp_header_size); skb_reset_transport_header(skb); ...... /* 构建TCP头部和校验和 */ th = (struct tcphdr *)skb->data; th->source = inet->inet_sport; th->dest = inet->inet_dport; th->seq = htonl(tcb->seq); th->ack_seq = htonl(rcv_nxt); tcp_options_write((__be32 *)(th + 1), tp, &opts); skb_shinfo(skb)->gso_type = sk->sk_gso_type; if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) { th->window = htons(tcp_select_window(sk)); tcp_ecn_send(sk, skb, th, tcp_header_size); } else { /* RFC1323: The window in SYN & SYN/ACK segments * is never scaled. */ th->window = htons(min(tp->rcv_wnd, 65535U)); } ...... icsk->icsk_af_ops->send_check(sk, skb); if (likely(tcb->tcp_flags & TCPHDR_ACK)) tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt); if (skb->len != tcp_header_size) { tcp_event_data_sent(tp, sk); tp->data_segs_out += tcp_skb_pcount(skb); tp->bytes_sent += skb->len - tcp_header_size; } if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq) TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS, tcp_skb_pcount(skb)); tp->segs_out += tcp_skb_pcount(skb); /* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */ skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb); skb_shinfo(skb)->gso_size = tcp_skb_mss(skb); /* Leave earliest departure time in skb->tstamp (skb->skb_mstamp_ns) */ /* Cleanup our debris for IP stacks */ memset(skb->cb, 0, max(sizeof(struct inet_skb_parm), sizeof(struct inet6_skb_parm))); err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl); ...... }
tcp_transmit_skb是tcp发送数据位于传输层的最后一步,这里首先对TCP数据段的头部进行了处理,然后调用了网络层提供的发送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);实现了数据的发送,自此,数据离开了传输层,传输层的任务也就结束了。
gdb调试验证如下:

4.2 接收端
- 传输层 TCP 处理入口在 tcp_v4_rcv 函数(位于 linux/net/ipv4/tcp ipv4.c 文件中),它会做 TCP header 检查等处理。
- 调用 _tcp_v4_lookup,查找该 package 的 open socket。如果找不到,该 package 会被丢弃。接下来检查 socket 和 connection 的状态。
- 如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。
接收端需要分析recv函数,与send类似,调用的是__sys_recvfrom
5 IP层流程
5.1 发送端
网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。网络层将数据链路层提供的帧组成数据包,包中封装有网络层包头,其中含有逻辑地址信息- -源站点和目的站点地址的网络地址。其主要任务包括 (1)路由处理,即选择下一跳 (2)添加 IP header(3)计算 IP header checksum,用于检测 IP 报文头部在传播过程中是否出错 (4)可能的话,进行 IP 分片(5)处理完毕,获取下一跳的 MAC 地址,设置链路层报文头,然后转入链路层处理。
IP 头:

IP 栈基本处理过程如下图所示:

- 首先,ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由。
- 接着,填充IP包的各个字段,比如版本、包头长度、TOS等。
- 中间的一些分片等,可参阅相关文档。基本思想是,当报文的长度大于mtu,gso的长度不为0就会调用 ip_fragment 进行分片,否则就会调用ip_finish_output2把数据发送出去。ip_fragment 函数中,会检查 IP_DF 标志位,如果待分片IP数据包禁止分片,则调用 icmp_send()向发送方发送一个原因为需要分片而设置了不分片标志的目的不可达ICMP报文,并丢弃报文,即设置IP状态为分片失败,释放skb,返回消息过长错误码。
- 接下来就用 ip_finish_ouput2 设置链路层报文头了。如果,链路层报头缓存有(即hh不为空),那就拷贝到skb里。如果没,那么就调用neigh_resolve_output,使用 ARP 获取。
具体代码分析如下:
入口函数是ip_queue_xmit,函数如下:

发现调用了__ip_queue_xmit函数:

发现调用了skb_rtable函数,实际上是开始找路由缓存,继续看:


发现调用ip_local_out进行数据发送:

发现调用__ip_local_out函数:

发现返回一个nf_hook函数,里面调用了dst_output,这个函数实质上是调用ip_finish__output函数:

发现调用__ip_finish_output函数:

如果分片就调用ip_fragment,否则就调用IP_finish_output2函数:

在构造好 ip 头,检查完分片之后,会调用邻居子系统的输出函数 neigh_output 进行输 出。neigh_output函数如下:

输出分为有二层头缓存和没有两种情况,有缓存时调用 neigh_hh_output 进行快速输 出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出。这个函数如下:

最后调用dev_queue_xmit函数进行向链路层发送包,到此结束。gdb验证如下:


5.2 接收端
- IP 层的入口函数在 ip_rcv 函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。
- ip_rcv_finish 函数会调用 ip_router_input 函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃:
- 如果是发到本机的话,调用 ip_local_deliver 函数,可能会做 de-fragment(合并多个 IP packet),然后调用 ip_local_deliver 函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而处理流程进入 TCP 栈。
- 如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用 dst_input 函数。该函数会 (1)处理 Netfilter Hook (2)执行 IP fragmentation (3)调用 dev_queue_xmit,进入链路层处理流程。
接收相对简单,入口在ip_rcv,这个函数如下:

里面调用ip_rcv_finish函数:

发现调用dst_input函数,实际上是调用ip_local_deliver函数:

如果分片,就调用ip_defrag函数,没有则调用ip_local_deliver_finish函数:

发现调用ip_protocol_deliver_rcu函数:

调用完毕之后进入tcp栈,调用完毕,通过gdb验证如下:

6 网络层流程
5.1 发送端
入口函数是 ip_queue_xmit ,ip_queue_xmit是 ip 层提供给 tcp 层发送回调函数。ip_queue_xmit() 完成面向连接套接字的包输出,当套接字处于连接状态时,所有从套接字发出的包都具有确定的路由, 无 需为每一个输出包查询它的目的入口,可将套接字直接绑定到路由入口上, 这由套接字的目的缓冲指针 (dst_cache)来完成。ip_queue_xmit()首先为输入包建立IP包头, 经过本地包过滤器后,再将IP包分片输出 (ip_fragment)。
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl) { return __ip_queue_xmit(sk,skb,fl,iner)sk(sk)-->tos); }
函数 ip_queue_xmit 调用函数 __ip_queue_xmit
/* Note: skb->sk can be different from sk, in case of tunnels */ int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl, __u8 tos) { struct inet_sock *inet = inet_sk(sk); struct net *net = sock_net(sk); struct ip_options_rcu *inet_opt; struct flowi4 *fl4; struct rtable *rt; struct iphdr *iph; int res; /* Skip all of this if the packet is already routed, * f.e. by something like SCTP. */ rcu_read_lock(); inet_opt = rcu_dereference(inet->inet_opt); fl4 = &fl->u.ip4; //獲取skb中的路由緩存 rt = skb_rtable(skb); if (rt) goto packet_routed; /* Make sure we can route this packet. */ rt = (struct rtable *)__sk_dst_check(sk, 0); if (!rt) { __be32 daddr;
Skb_rtable(skb) 获取 skb 中的路由缓存,然后判断是否有缓存,如果有缓存就直接进行 packet_routed 函数,否则就 执行 ip_route_output_ports 查找路由缓存。
/* 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;
最后调用 ip_local_out 得到返回值res:
int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb) { int err; err = __ip_local_out(net, sk, skb); if (likely(err == 1)) err = dst_output(net, sk, skb); return err; }
同函数 ip_queue_xmit 一样, ip_local_out 函数内部调用 __ip_local_out 。
int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb) { struct iphdr *iph = ip_hdr(skb); iph->tot_len = htons(skb->len); ip_send_check(iph); /* if egress device is enslaved to an L3 master device pass the * skb to its handler for processing */ skb = l3mdev_ip_out(sk, skb); if (unlikely(!skb)) return 0; skb->protocol = htons(ETH_P_IP); return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, net, sk, skb, NULL, skb_dst(skb)->dev, }
发现返回一个 nf_hook 函数,里面调用了 dst_output ,这个函数实质上是调用 ip_finish__output 函数, ip_finish__output 函数内部在调用 __ip_finish_output 函数
static int __ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb) { unsigned int mtu; #if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM) /* Policy lookup after SNAT yielded a new policy */ if (skb_dst(skb)->xfrm) { IPCB(skb)->flags |= IPSKB_REROUTED; return dst_output(net, sk, skb); } #endif mtu = ip_skb_dst_mtu(sk, skb); if (skb_is_gso(skb)) return ip_finish_output_gso(net, sk, skb, mtu); if (skb->len > mtu || (IPCB(skb)->flags & IPSKB_FRAG_PMTU)) return ip_fragment(net, sk, skb, mtu, ip_finish_output2); return ip_finish_output2(net, sk, skb); }
如果分片就调用 ip_fragment ,否则就调用 ip_fragment 函数:
static int ip_fragment(struct net *net, struct sock *sk, struct sk_buff *skb, unsigned int mtu, int (*output)(struct net *, struct sock *, struct sk_buff *)) struct iphdr *iph = ip_hdr(skb); if ((iph->frag_off & htons(IP_DF)) == 0) return ip_do_fragment(net, sk, skb, output); if (unlikely(!skb->ignore_df || (IPCB(skb)->frag_max_size && IPCB(skb)->frag_max_size > mtu))) { IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS); icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(mtu)); kfree_skb(skb); return -EMSGSIZE; } return ip_do_fragment(net, sk, skb, output); }
在构造好 ip 头,检查完分片之后,会调用邻居子系统的输出函数 neigh_output 进行输出。最后调用 dev_queue_xmit 函数进行向下层发送包。
5.2 接收端
IP 层的入口函数在 ip_rcv 函数。该函数首先会做包括 package checksum 在内的各种检查,如果需 要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。ip_rcv_finish 函数会调用 ip_router_input 函数,进入 路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会 被发到本机还是会被转发还是丢弃。
/* * IP receive entry point */ int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev) { struct net *net = dev_net(dev); skb = ip_rcv_core(skb, net); if (skb == NULL) return NET_RX_DROP; return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, net, NULL, skb, dev, NULL, ip_rcv_finish); }
ip_rcv 函数内部会调用 ip_rcv_finish 函数:
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb) { struct net_device *dev = skb->dev; int ret; /* if ingress device is enslaved to an L3 master device pass the * skb to its handler for processing */ skb = l3mdev_ip_rcv(skb); if (!skb) return NET_RX_SUCCESS; ret = ip_rcv_finish_core(net, sk, skb, dev); if (ret != NET_RX_DROP) ret = dst_input(skb); return ret; }
dst_input 函数会进一步调用 ip_local_deliver 函数:
int ip_local_deliver(struct sk_buff *skb) { /* * Reassemble IP fragments. */ struct net *net = dev_net(skb->dev); if (ip_is_fragment(ip_hdr(skb))) { if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER)) return 0; } return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, net, NULL, skb, skb->dev, NULL, } ip_local_deliver_finish);
最后调用 ip_protocol_deliver_rcu 函数:
void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol) { const struct net_protocol *ipprot; int raw, ret; resubmit: raw = raw_local_deliver(skb, protocol); ipprot = rcu_dereference(inet_protos[protocol]); if (ipprot) { if (!ipprot->no_policy) { if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { kfree_skb(skb); return; } nf_reset_ct(skb); } ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv, skb); if (ret < 0) { protocol = -ret; goto resubmit; } __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS); } else { if (!raw) { if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { __IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS); icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PROT_UNREACH, 0); } kfree_skb(skb); } else { __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS); consume_skb(skb); } } }

7 时序图展示和总结

本次实验主要是通过分析Linux内核源代码,一步步的通过gdb进行调试函数调用链,最终清楚了tcp/ip协议栈的调用过程。
8 参考资料
1 《庖丁解牛Linux内核分析》
2 https://www.cnblogs.com/myguaiguai/p/12069485.html
3 https://www.cnblogs.com/jmilkfan-fanguiju/p/12789808.html#Linux__23
浙公网安备 33010602011771号