网络程序设计调研报告

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

姓名:柴浩宇

学号:SA20225105

班级:软设1班

2021年1月

调研要求

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

目录

1 调研要求

2 目录

3 Linux概述

  3.1 Linux操作系统架构简介

  3.2 协议栈简介

  3.3 Linux内核协议栈的实现

4 本次调研采取的代码简介

5 应用层流程

  5.1 发送端

  5.2 接收端

6 传输层流程

  6.1 发送端

  6.2 接收端

7 IP层流程

  7.1 发送端

  7.2 接收端

8 数据链路层流程

  8.1 发送端

  8.2 接收端

9 物理层流程

  9.1 发送端

  9.2 接收端

10 时序图展示和总结

11 参考资料

正文

3 Linux概述

  3.1 Linux操作系统架构简介

Linux操作系统总体上由Linux内核和GNU系统构成,具体来讲由4个主要部分构成,即Linux内核、Shell、文件系统和应用程序。内核、Shell和文件系统构成了操作系统的基本结构,使得用户可以运行程序、管理文件并使用系统。

内核是操作系统的核心,具有很多最基本功能,如虚拟内存、多任务、共享库、需求加载、可执行程序和TCP/IP网络功能。我们所调研的工作,就是在Linux内核层面进行分析。

  3.2 协议栈简介

  OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织)组织在1985年研究的网络互连模型。
        ISO为了更好的使网络应用更为普及,推出了OSI参考模型。其含义就是推荐所有公司使用这个规范来控制网络。这样所有公司都有相同的规范,就能互联了。
OSI定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),即ISO开放互连系统参考模型。如下图。

        每一层实现各自的功能和协议,并完成与相邻层的接口通信。OSI的服务定义详细说明了各层所提供的服务。某一层的服务就是该层及其下各层的一种能力,它通过接口提供给更高一层。各层所提供的服务与这些服务是怎么实现的无关。
  osi七层模型已经成为了理论上的标准,但真正运用于实践中的是TCP/IP五层模型。
  TCP/IP五层协议和osi的七层协议对应关系如下:

在每一层实现的协议也各不同,即每一层的服务也不同.下图列出了每层主要的协议。

  3.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网络协议栈是一个严格分层的结构,其中的每一层都执行相对独立的功能,结构非常清晰。
  其中的两个“无关”层的设计非常棒,通过这两个“无关”层,其协议栈可以非常轻松的进行扩展。在我们自己的软件设计中,可以吸收这种设计方法。

4 本次调研采取的代码简介

本文采用的测试代码是一个非常简单的基于socket的客户端服务器程序,打开服务端并运行,再开一终端运行客户端,两者建立连接并可以发送hello\hi的信息,server端代码如下:

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

client端代码如下:

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

简单来说,主要流程如下图所示:

5 应用层流程

  5.1 发送端

  1. 网络应用调用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。
  2. 对于 TCP socket 来说,应用调用 connect()API ,使得客户端和服务器端通过该 socket 建立一个虚拟连接。在此过程中,TCP 协议栈通过三次握手会建立 TCP 连接。默认地,该 API 会等到 TCP 握手完成连接建立后才返回。在建立连接的过程中的一个重要步骤是,确定双方使用的 Maxium Segemet Size (MSS)。因为 UDP 是面向无连接的协议,因此它是不需要该步骤的。
  3. 应用调用 Linux Socket 的 send 或者 write API 来发出一个 message 给接收端
  4. sock_sendmsg 被调用,它使用 socket descriptor 获取 sock struct,创建 message header 和 socket control message
  5. _sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数。
    1. 对于 TCP ,调用 tcp_sendmsg 函数。
    2. 对于 UDP 来说,userspace 应用可以调用 send()/sendto()/sendmsg() 三个 system call 中的任意一个来发送 UDP message,它们最终都会调用内核中的 udp_sendmsg() 函数。

  

下面我们具体结合Linux内核源码进行一步步仔细分析:

根据上述分析可知,发送端首先创建socket,创建之后会通过send发送数据。具体到源码级别,会通过send,sendto,sendmsg这些系统调用来发送数据,而上述三个函数底层都调用了sock_sendmsg。见下图:

我们再跳转到__sys_sendto看看这个函数干了什么:

我们可以发现,它创建了两个结构体,分别是:struct msghdr msg和struct iovec iov,这两个结构体根据命名我们可以大致猜出是发送数据和io操作的一些信息,如下图:

                                    

我们再来看看__sys_sendto调用的sock_sendmsg函数执行了什么内容:

发现调用了sock_sendmsg_nosec函数:

发现调用了inet_sendmsg函数:

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

刚好符合我们的分析。

  5.2 接收端

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

 

我们结合源码进行仔细分析:

接收端调用的是__sys_recvfrom函数:

__sys_recvfrom函数具体如下:

发现它调用了sock_recvmsg函数:

发现它调用了sock_recvmsg_nosec函数:

发现它调用了inet_recvmsg函数:

最后调用的是tcp_recvmsg这个系统调用。至此接收端调用分析完毕。

下面用gdb打断点进行验证:

验证结果刚好符合我们的调研。

6 传输层流程

  6.1 发送端

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

TCP 栈简要过程:

  1. tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开始 segement 发送流程。
  2. 构造 TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从 userspace buffer 中拷贝 packet 的数据到 skb 的 buffer。
  3. 构造 TCP header。
  4. 计算 TCP 校验和(checksum)和 顺序号 (sequence number)。
    1. TCP 校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。TCP校验和覆盖 TCP 首部和 TCP 数据。
    2. TCP的校验和是必需的
  5. 发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程。

UDP 栈简要过程:

  1. UDP 将 message 封装成 UDP 数据报
  2. 调用 ip_append_data() 方法将 packet 送到 IP 层进行处理。

 下面我们结合代码依次分析:

根据我们对应用层的追查可以发现,传输层也是先调用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,
......

而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_sendmsg_locked中,完成的是将所有的数据组织成发送队列,这个发送队列是struct sock结构中的一个域sk_write_queue,这个队列的每一个元素是一个skb,里面存放的就是待发送的数据。然后调用了tcp_push()函数。结构体struct sock如下:

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

在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调试验证如下:

  6.2 接收端

  1. 传输层 TCP 处理入口在 tcp_v4_rcv 函数(位于 linux/net/ipv4/tcp ipv4.c 文件中),它会做 TCP header 检查等处理。
  2. 调用 _tcp_v4_lookup,查找该 package 的 open socket。如果找不到,该 package 会被丢弃。接下来检查 socket 和 connection 的状态。
  3. 如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。

对于传输层的代码阶段,我们需要分析recv函数,这个与send类似,调用的是__sys_recvfrom,整个函数的调用路径与send非常类似:

int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
struct sockaddr __user *addr, int __user *addr_len)
{
......
err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
.....
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);
.....
}

__sys_recvfrom调用了sock_recvmsg来接收数据,整个函数实际调用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);,同样,根据tcp_prot结构的初始化,调用的其实是tcp_rcvmsg

接受函数比发送函数要复杂得多,因为数据接收不仅仅只是接收,tcp的三次握手也是在接收函数实现的,所以收到数据后要判断当前的状态,是否正在建立连接等,根据发来的信息考虑状态是否要改变,在这里,我们仅仅考虑在连接建立后数据的接收。

tcp_rcvmsg函数如下:

int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
......
if (sk_can_busy_loop(sk) && skb_queue_empty(&sk->sk_receive_queue) &&
(sk->sk_state == TCP_ESTABLISHED))
sk_busy_loop(sk, nonblock); lock_sock(sk);
.....
if (unlikely(tp->repair)) {
err = -EPERM;
if (!(flags & MSG_PEEK))
goto out; if (tp->repair_queue == TCP_SEND_QUEUE)
goto recv_sndq; err = -EINVAL;
if (tp->repair_queue == TCP_NO_QUEUE)
goto out;
......
last = skb_peek_tail(&sk->sk_receive_queue);
skb_queue_walk(&sk->sk_receive_queue, skb) {
last = skb;
......
if (!(flags & MSG_TRUNC)) {
err = skb_copy_datagram_msg(skb, offset, msg, used);
if (err) {
/* Exception. Bailout! */
if (!copied)
copied = -EFAULT;
break;
}
} *seq += used;
copied += used;
len -= used; tcp_rcv_space_adjust(sk);

这里共维护了三个队列:prequeuebacklogreceive_queue,分别为预处理队列,后备队列和接收队列,在连接建立后,若没有数据到来,接收队列为空,进程会在sk_busy_loop函数内循环等待,知道接收队列不为空,并调用函数数skb_copy_datagram_msg将接收到的数据拷贝到用户态,实际调用的是__skb_datagram_iter,这里同样用了struct msghdr *msg来实现。__skb_datagram_iter函数如下:

int __skb_datagram_iter(const struct sk_buff *skb, int offset,
struct iov_iter *to, int len, bool fault_short,
size_t (*cb)(const void *, size_t, void *, struct iov_iter *),
void *data)
{
int start = skb_headlen(skb);
int i, copy = start - offset, start_off = offset, n;
struct sk_buff *frag_iter; /* 拷贝tcp头部 */
if (copy > 0) {
if (copy > len)
copy = len;
n = cb(skb->data + offset, copy, data, to);
offset += n;
if (n != copy)
goto short_copy;
if ((len -= copy) == 0)
return 0;
} /* 拷贝数据部分 */
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
int end;
const skb_frag_t *frag = &skb_shinfo(skb)->frags[i]; WARN_ON(start > offset + len); end = start + skb_frag_size(frag);
if ((copy = end - offset) > 0) {
struct page *page = skb_frag_page(frag);
u8 *vaddr = kmap(page); if (copy > len)
copy = len;
n = cb(vaddr + frag->page_offset +
offset - start, copy, data, to);
kunmap(page);
offset += n;
if (n != copy)
goto short_copy;
if (!(len -= copy))
return 0;
}
start = end;
}

拷贝完成后,函数返回,整个接收的过程也就完成了。
用一张函数间的相互调用图可以表示:

通过gdb调试验证如下:

Breakpoint 1, __sys_recvfrom (fd=5, ubuf=0x7ffd9428d960, size=1024, flags=0,
addr=0x0 <fixed_percpu_data>, addr_len=0x0 <fixed_percpu_data>)
at net/socket.c:1990
1990 {
(gdb) c
Continuing. Breakpoint 2, sock_recvmsg (sock=0xffff888006df1900, msg=0xffffc900001f7e28,
flags=0) at net/socket.c:891
891 {
(gdb) c
Continuing. Breakpoint 3, tcp_recvmsg (sk=0xffff888006479100, msg=0xffffc900001f7e28,
len=1024, nonblock=0, flags=0, addr_len=0xffffc900001f7df4)
at net/ipv4/tcp.c:1933
1933 {
(gdb) c
Breakpoint 1, __sys_recvfrom (fd=5, ubuf=0x7ffd9428d960, size=1024, flags=0,
addr=0x0 <fixed_percpu_data>, addr_len=0x0 <fixed_percpu_data>)
at net/socket.c:1990
1990 {
(gdb) c
Continuing. Breakpoint 2, sock_recvmsg (sock=0xffff888006df1900, msg=0xffffc900001f7e28,
flags=0) at net/socket.c:891
891 {
(gdb) c
Continuing. Breakpoint 3, tcp_recvmsg (sk=0xffff888006479100, msg=0xffffc900001f7e28,
len=1024, nonblock=0, flags=0, addr_len=0xffffc900001f7df4)
at net/ipv4/tcp.c:1933
1933 {
(gdb) c
Continuing. Breakpoint 4, __skb_datagram_iter (skb=0xffff8880068714e0, offset=0,
to=0xffffc900001efe38, len=2, fault_short=false,
cb=0xffffffff817ff860 <simple_copy_to_iter>, data=0x0 <fixed_percpu_data>)
at net/core/datagram.c:414
414 {

符合我们之前的分析。

7 IP层流程

  7.1 发送端

网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。网络层将数据链路层提供的帧组成数据包,包中封装有网络层包头,其中含有逻辑地址信息- -源站点和目的站点地址的网络地址。其主要任务包括 (1)路由处理,即选择下一跳 (2)添加 IP header(3)计算 IP header checksum,用于检测 IP 报文头部在传播过程中是否出错 (4)可能的话,进行 IP 分片(5)处理完毕,获取下一跳的 MAC 地址,设置链路层报文头,然后转入链路层处理。

IP 头:

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

  1. 首先,ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由。
  2. 接着,填充IP包的各个字段,比如版本、包头长度、TOS等。
  3. 中间的一些分片等,可参阅相关文档。基本思想是,当报文的长度大于mtu,gso的长度不为0就会调用 ip_fragment 进行分片,否则就会调用ip_finish_output2把数据发送出去。ip_fragment 函数中,会检查 IP_DF 标志位,如果待分片IP数据包禁止分片,则调用 icmp_send()向发送方发送一个原因为需要分片而设置了不分片标志的目的不可达ICMP报文,并丢弃报文,即设置IP状态为分片失败,释放skb,返回消息过长错误码。
  4. 接下来就用 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验证如下:

  7.2 接收端

  1. IP 层的入口函数在 ip_rcv 函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。
  2. ip_rcv_finish 函数会调用 ip_router_input 函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃:
    1. 如果是发到本机的话,调用 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 栈。
    2. 如果需要转发 (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验证如下:

8 数据链路层流程

  8.1 发送端

功能上,在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列。数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)。数据链路层协议的代表包括:SDLC、HDLC、PPP、STP、帧中继等。

实现上,Linux 提供了一个 Network device 的抽象层,其实现在 linux/net/core/dev.c。具体的物理网络设备在设备驱动中(driver.c)需要实现其中的虚函数。Network Device 抽象层调用具体网络设备的函数。

发送端调用dev_queue_xmit,这个函数实际上调用__dev_queue_xmit:

发现它调用了dev_hard_start_xmit函数:

调用xmit_one:

调用trace_net_dev_start_xmit,实际上调用__net_dev_start_xmit函数:

到此,调用链结束。gdb调试如下:

  8.2 接收端

简要过程:

  1. 一个 package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。
  2. 网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中;从数据帧中提取出一些信息,并设置 skb_buff 相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;
  3. 终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。
  4. 内核 2.5 中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:(1) 通过以前的函数netif_rx;(2)通过NAPI机制。该中断处理程序调用 Network device的 netif_rx_schedule 函数,进入软中断处理流程,再调用 net_rx_action 函数。
  5. 该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从 rx_ring 中被删除,进入 netif _receive_skb 处理流程。
  6. netif_receive_skb 是链路层接收数据报的最后一站。它根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。

入口函数是net_rx_action:

发现调用napi_poll,实质上调用napi_gro_receive函数:

napi_gro_receive 会直接调用 netif_receive_skb_core。而它会调用__netif_receive_skb_one_core,将数据包交给上层 ip_rcv 进行处理。

调用结束之后,通过软中断通知CPU,至此,调用链结束。gdb验证如下:

9 物理层流程

  9.1 发送端

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

  9.2 接收端

  1. 一个 package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。
  2. 网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中;从数据帧中提取出一些信息,并设置 skb_buff 相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;
  3. 终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。
  4. 内核 2.5 中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:(1) 通过以前的函数netif_rx;(2)通过NAPI机制。该中断处理程序调用 Network device的 netif_rx_schedule 函数,进入软中断处理流程,再调用 net_rx_action 函数。
  5. 该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从 rx_ring 中被删除,进入 netif _receive_skb 处理流程。
  6. netif_receive_skb 是链路层接收数据报的最后一站。它根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。

10 时序图展示和总结


时序图如下:

本次实验主要是通过分析Linux内核源代码,一步步的通过gdb进行调试函数调用链,最终清楚了tcp/ip协议栈的调用过程。因为时间有限,部分细节可能会有错误,希望读者多加指正。

11 参考资料

1 《庖丁解牛Linux内核分析》

2  https://www.cnblogs.com/myguaiguai/p/12069485.html

3  https://www.cnblogs.com/jmilkfan-fanguiju/p/12789808.html#Linux__23

TCP/IP协议栈在Linux内核中的运行时序分析的更多相关文章

  1. Linux TCP/IP调优-Linux内核参数注释

    固定文件的内核参数 下列文件所在目录: /proc/sys/net/ipv4/ 名称 默认值 建议值 描述 tcpsyn_retries 5 1 对于一个新建连接,内核要发送多少个SYN连接请求才决定 ...

  2. 理解TCP/IP协议栈之HTTP2.0

    1 前言 前面写了10多篇关于Redis底层实现.工程架构.实际应用的文章,感兴趣的读者可以进行阅读,如有问题欢迎交流: 1.Redis面试热点之底层实现篇-12.Redis面试热点之底层实现篇-23 ...

  3. windows下重新安装TCP/IP协议栈

    一.windows重装TCP/IP协议     前两天在windows下安装开发环境的时候,把系统的TCP/IP协议栈给搞跪了,导致系统无法ping localhost.无法在程序中创建socket等 ...

  4. TCP/IP协议栈源码图解分析系列10:linux内核协议栈中对于socket相关API的实现

    题记:本系列文章的目的是抛开书本从Linux内核源代码的角度详细分析TCP/IP协议栈内核相关技术 轻松搞定TCP/IP协议栈,原创文章欢迎交流, byhankswang@gmail.com linu ...

  5. 用virtualbox+模拟串口+CDT调试linux内核 TCP/IP协议栈-起步

    经常有人问一台机器如何将hello经网络发送给另一台机器,我确实是不知道,只能看代码了. 说明:本人对内核的研究学习也是刚刚起步,有很多不了解的,所以文中可能会有一些"一本正经的胡扯&quo ...

  6. CVE-2019-11477:Linux 内核中TCP协议栈整数溢出漏洞详细分析 代码卫士 今天

    CVE-2019-11477:Linux 内核中TCP协议栈整数溢出漏洞详细分析 代码卫士 今天

  7. Linux TCP/IP 协议栈之 Socket 的实现分析(一)

    内核版本:2.6.37参考[作者:kendo的文章(基于内涵版本2.6.12)] 第一部份 Socket套接字的创建 socket 并不是 TCP/IP协议的一部份. 从广义上来讲,socket 是U ...

  8. linux OSI七层模型、TCP/IP协议栈及每层结构大揭秘

    学习Linux,就算是像小编我这样的小萌新,也知道OSI模型.什么?!你不知道!!! 好吧,这篇秘籍拿走,不谢~~~ 一.两个协议 (1)OSI 协议模型(7层)国际协议    PDU:协议数据单元对 ...

  9. Linux 从网卡到TCP IP协议栈数据流跟踪与审计

    前沿 在学代码审计,然后最近做Linux协议栈的审计,发现Linux不愧是一个久经考验的系统,本来以为可以找到个DoS的,结果发现其在TCP/IP协议栈的链路层实现,利用了各种技术,用来提高性能与安全 ...

随机推荐

  1. linux 上安装部署python

    一般在linux中使用python 需要安装pyenv 进行版本控制 因为linux6.9自带的Python是2.6的 同时很多命令都是基于2.6开发的 所以系统环境不能改 我们要开发 只能用pyen ...

  2. 模块urllib requests json xml configparser 学习笔记

    发起http请求 获取返回值 返回值是字符串 第三方模块安装 pip install requests 返回值格式 xml  html  jaon json 功能  loads   字符串>&g ...

  3. nginx 静态化合集(去掉index.php目录)

    访问某域名时,去掉index.php目录时达到效果一样 如:www.test1/index.php/test2跟www.test1/test2效果一致 在vhosts.conf中加重写就可以了 loc ...

  4. proxySQL with SemiSync

    环境信息 hostname IP port role comm ms81 192.168.188.81 3399 master ms82 192.168.188.82 3399 slave ms83 ...

  5. angular8 在componet里面跳转新的地址页面

    this.router.navigate(['/teacher/course/detail/' + id]);

  6. 纯Python绘制艺术感满满的山脊地图,创意满分

    1 简介 下面的这幅图是英国摇滚乐队 Joy Division 在1979年发行的其第一张录音室专辑 Unknown Pleasures 的封面,由艺术家 Peter Saville 基于射电脉冲星信 ...

  7. VS批量添加多个文件

    步骤 1.设置项目,显示所有文件.2.点击项目,右键在资源管理器中打开.3.将要导入的文件复制到对应的文件中去.4.在解决方案中就可以看到刚才添加的多个文件.5.在新加的文件夹上右键,包括在项目中.

  8. spring-quartz整合

    摘要 spring ,springboot整合quartz-2.3.2,实现spring管理jobBean 本文不涉及 JDBC存储的方式,springboot yml配置也没有 可自行百度 谷歌 本 ...

  9. Redis基础篇(三)持久化:AOF日志

    Redis是内存数据库,但是一旦服务器宕机,内存中的数据将会全部丢失. 最简单的恢复方式是从后端数据库恢复,但这种方式有两个问题: 频繁访问数据库,会给数据库带来巨大的压力: 从数据库中读取相比从Re ...

  10. C#脚本引擎CS-Script

    最近想要在程序中嵌入一个C#脚本引擎,在.NET Framework时代用过一个叫做CS-Script的东西,感觉还是不错,发现现在也支持.NET Core了,试着嵌入一下. 比较 要说能够运行C#脚 ...