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

 

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

 

目录

1.调研要求

2.网络体系结构

3.SOCKET

4.send过程

  4.1应用层

  4.2传输层

  4.3网络层

  4.4数据链路层

  4.5物理层

5.recv过程

  5.1应用层

  5.2传输层

  5.3网络层

  5.4数据链路层

6.时序图

 

 1.调研要求

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

 

2.网络体系结构

2.1ISO/OSI参考模型

  ISO/OSI参考模型是国际标准化组织为开放系统通信(OSI)而提出的,它描述了基于数字数据的计算机辅助系统之间通信的一般抽象模型。它还可以作为一个框架,用于开发通信标准。

  ISO/OSI模型由7个层组成,一个层向其更高层提供特定的服务。ISO/OSI模型并不描述特定系统的实现,而仅仅定义各个层的任务。因此,ISO/OSI模型经常被引用为基本的参考模型。事实上,ISO/OSI理论知识通常是现代计算机网络设计和构建的基础,尽管他并不是一个完美的模型。在后文我们将把ISO/OSI模型与更搞笑的TCP/IP模型进行比较,从而可以发现前者的优点和缺点。

  ISO/OSI参考模型的七层和各个层的功能如下:

  ①物理层:物理层处理在物理介质上传输的数据位。更具体地说,就是(非结构化的)位序列被转换为物理信号并在物理介质上传输。物理层定义特殊的编码方式、硬件连接和介质类型。

  ②数据链路层:这一层规定如何在通过介质之间连接的两个站点之间传输数据,发送系统将数据组织成为帧。然后对它们进行back to back 的传输。如果发生错误,数据链路层负责检测此类错误,并重新传输数据帧。此外,两个系统之间的数据流应该受到监管,以确保接收方不会收到过多的流控制。数据链路层协议的范例包括高级数据链路控制协议(HDLC)、串行线路英特网协议(SLIP)和点对点协议(PPP)。在本地网内,数据链路层通常使用共享介质来承担常规访问任务。在这种情况下,数据链路层被分为介质访问控制(MAC)层和逻辑链路控制(LLC)层。

  ③网络层:网络层负责建立通信网络的所有系统之间的连接。因此,网络层主要处理数据的交换和前送(forwarding)(如路由、将数据单元转换为相应的数据链路层可接受的大小或确保各个服务的质量)。

  ④传输层:传输层管理应用之间的数据传输(如发送方应用和接收方应用之间的数据传输)。此外,它还负责提供传输服务、控制终端系统之间的数据流以及确保数据的正确性和有序性。

  ⑤会话层:会话层处理传输链路上报文的结构化交换(structured exchange of messages)。例如,它控制一个会话内是否允许数据同时双向传输或者只有通信双方的其中一方才有权传输数据。对于后一种情况,会话层管理数据传输的权力。

  ⑥表示层:表示层管理被转换数据的表示,它与通信计算机系统的类型无关。许多操作系统使用不同形式的字符表示、数字表示以及其他表示。为了确保不同的系统之间能够交换数据,表示层将数据转换为标准形式。

  ⑦应用层:这一层为不同的应用使用特定的协议,使用低级层来完成它们的任务——例如,应用层包含的协议有电子邮件协议、文件传输协议和远程过程调用协议等等。

2.2TCP/IP参考模型

  因特网参考模型的命名约定的基础是两个最重要的因特网协议——传输控制协议(TCP)和网际协议(IP)。前面描述的七层ISO/OSI参考模型在因特网诞生之前就已经制定了。此外,七层参考模型将会话协议组织成为整个层,由于计算机系统已经从大型主机系统转变为个人工作站,因此这一层的重要性降低了很多。

  因此,开发TCP/IP的研究人员发明了一个新的分层模型。本节将简短地描述这个新的分层模型。

  TCP/IP分层模型也称为因特网参考模型,它包括下列层:

  ①应用层:应用层组合了所有面向应用的任务(例如ISO/OSI模型第5层至第7层的属性)。应用层的协议包括Telnet(虚拟终端)、FTP(文件传输)和SMTP(发送电子邮件)。较新的一些协议包括DNS(域名系统)和HTTP(超文本传输协议)。

  ②传输层:与ISO/OSI参考模型一样,TCP/IP参考模型中的传输层允许终端系统应用之间进行通信。为此,TCP/IP参考模型定义了两个基本协议:传输控制协议(TCP)和用户数据报(UDP)。TCP是可靠的、面向连接的协议,它将字节流通过因特网传输至其他计算机上而不发生错误。UDP是不可靠的、无连接的协议,但是在很多情况下,它比复杂的TCP更适用(如传输多媒体数据)。

  ③网际层:TCP/IP参考模型的网际层定义了网际协议(IP),包括两个附属协议:因特网控制报文协议(ICMP)和因特网组管理协议(IGMP)。网际层的主要功能是通过网络将IP包从发送方传输至接受方,在传输过程中,包的路由扮演了十分重要的角色。因特网控制报文协议(ICMP)是每个IP实现的必不可少的部分,它为网际协议传输诊断信息和差错信息。因特网组管理协议(IGMP)用于管理通信组。

  ④网络接口层:这一层组合了ISO/OSI参考模型的两个层。他处理网络适配器和它们的驱动程序,网络适配器用于在局域网(如以太网和令牌环网等)上指定的最大长度范围内交换数据包,或者用于在广域网(如ISDN和ATM等)上交换数据包。

2.3Linux网络内核的组成模块

   Linux网络内核参照网络协议体系实现了网络互连功能。下图是Linux实现的TCP/IP协议体系框图。

  ①套接字接口。网络内核最顶层是支持应用程序开发的函数接口,这是一系列标准函数,即套接字接口。套接字支持多种不同类型的协议族:UNIX域协议族、TCP/IP协议族、IPX协议族等。对于TCP/IP协议族对应的套接字,该套接字又包括三个基本类型:SOCK_STREAM、SOCK_DGRAM和SOCK_RAW。通过SOCK_STREAM套接字可以访问TCP协议、SOCK_DGRAM可以访问UDP协议、SOCK_RAW套接字可以直接访问IP协议。可见,套接字接口是网络内核的入口。

  ②传输层和网络层。套接字往下依次是传输层和网络层。传输层包括标准的TCP和UDP协议模块,二网络层包括标准的IP协议模块。

  ③数据链路层。对于需要逻辑链路的网络,数据链路层提供独立的逻辑链路协议模块,比如PPP、SLIP等。对于以太网,该层比较简单,主要的以太网协议实现被集成到底层的网卡驱动中。

  ④网络设备驱动。由于物理特性的差异,因此不同的网络设备采用不同的设备驱动。比如,TTY设备和以太网卡采用各自的驱动模块。

 

3.SOCKET

3.1 SOCKET简介

  Socket,又称套接字,是Linux跨进程通信(IPC,Inter Process Communication)方式的一种。相比于其他IPC方式,Socket不仅仅可以做到同一台主机内跨进程通信,它还可以做到不同主机间的跨进程通信。TCP/IP协议族中,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

  Socket根据通信协议的不同还可以分为3种:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)及原始套接字。

  • 流式套接字(SOCK_STREAM):最常见的套接字,使用TCP协议,提供可靠的、面向连接的通信流。保证数据传输是正确的,并且是顺序的。应用于Telnet远程连接、WWW服务等。
  • 数据报套接字(SOCK_DGRAM):使用UDP协议,提供无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠性。使用UDP的应用程序要有自己的对数据进行确认的协议。
  • 原始套接字:允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议实现的测试等。原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。

3.2 SOCKET数据结构

  struct socket:这个是基本的BSD socket,面向用户空间,应用程序通过系统调用开始创建的socket都是该结构体,它是基于虚拟文件系统创建出来的。

 1 struct socket {
 2     socket_state        state; //socket的状态,比如CONNECTED
 3 
 4     short            type;//类型,比如TCP下使用的流式套接字SOCK_STREAM
 5 
 6     unsigned long        flags;//标志位,负责一些特殊的设置,比如SOCK_ASYNC_NOSPACE
 7 
 8     struct socket_wq __rcu    *wq;//等待队列
 9 
10     struct file        *file;//与socket相关的指针列表
11     struct sock        *sk;//负责记录协议相关内容
12     const struct proto_ops    *ops;//采用了和超级块设备操作表一样的逻辑,专门设置了一个数据结构来记录其允许的操作
13 };

  socket()本质上是一个glibc中的函数,执行实际上是是调用sys_socketcall()系统调用。sys_socketcall()是几乎所有socket相关函数的入口,即是说,bind,connect等等函数都需要sys_socketcall()作为入口。该系统调用代码如下:

 1 SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
 2 {
 3     unsigned long a[AUDITSC_ARGS];
 4     unsigned long a0, a1;
 5     int err;
 6     unsigned int len;
 7 
 8     if (call < 1 || call > SYS_SENDMMSG)
 9         return -EINVAL;
10     call = array_index_nospec(call, SYS_SENDMMSG + 1);
11 
12     len = nargs[call];
13     if (len > sizeof(a))
14         return -EINVAL;
15 
16     /* copy_from_user should be SMP safe. */
17     if (copy_from_user(a, args, len))
18         return -EFAULT;
19 
20     err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
21     if (err)
22         return err;
23 
24     a0 = a[0];
25     a1 = a[1];
26 
27     switch (call) {
28     case SYS_SOCKET:
29         err = __sys_socket(a0, a1, a[2]);
30         break;
31     case SYS_BIND:
32         err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
33         break;
34     case SYS_CONNECT:
35         err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
36         break;
37     case SYS_LISTEN:
38         err = __sys_listen(a0, a1);
39         break;
40     case SYS_ACCEPT:
41         err = __sys_accept4(a0, (struct sockaddr __user *)a1,
42                     (int __user *)a[2], 0);
43         break;
44     ...
45     ...
46     default:
47         err = -EINVAL;
48         break;
49     }
50     return err;
51 }

3.3 SOCKET工作流程

  套接字工作过程如下图所示:服务器首先启动,通过调用socket()建立一个套接字,然后调用bind()将该套接字和本地网络地址联系在一起,再调用listen()使套接字做好侦听的准备,并规定它的请求队列的长度,之后就调用accept()来接收连接。客户端在建立套接字后就可调用connect()和服务器建立连接。连接一旦建立,客户机和服务器之间就可以通过调用read()和write()来发送和接收数据。最后,待数据传送结束后,双方调用close()关闭套接字。

 

 4. send过程

4.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。

  ②对于 TCP socket 来说,应用调用 connect()API ,使得客户端和服务器端通过该 socket 建立一个虚拟连接。在此过程中,TCP 协议栈通过三次握手会建立 TCP 连接。默认地,该 API 会等到 TCP 握手完成连接建立后才返回。在建立连接的过程中的一个重要步骤是,确定双方使用的 Maxium Segemet Size (MSS)。

  ③应用调用 Linux Socket 的 send 或者 write API 来发出一个 message 给接收端

  ④sock_sendmsg 被调用,它使用 socket descriptor 获取 sock struct,创建 message header 和 socket control message

  ⑤_sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数。

  ⑥对于 TCP ,调用 tcp_sendmsg 函数。

gdb调试及源码分析

  send 和sendto系统调用。

 1 SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
 2         unsigned int, flags, struct sockaddr __user *, addr,
 3         int, addr_len)
 4 {
 5     return __sys_sendto(fd, buff, len, flags, addr, addr_len);
 6 }
 7 
 8 SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
 9         unsigned int, flags)
10 {
11     return __sys_sendto(fd, buff, len, flags, NULL, 0);
12 }

  在内核的实现中,send和sendto系统调用最终都会调用到内核函数__sys_sendto()。传入的参数分别为 fd       socket文件描述符
            buff     指向需要发送的数据
            len      需要发送的数据的长度
            flags    标志位
            addr     数据报文要发送的对方端点的地址信息
            addr_len 地址信息的长度

  sys_sendto根据传入的描述符fd,找到对应的struct socket结构体,然后构建内核的消息结构struct msghdr。

 1 int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags,
 2          struct sockaddr __user *addr,  int addr_len)
 3 {
 4     struct socket *sock;
 5     struct sockaddr_storage address;
 6     int err;
 7     struct msghdr msg;
 8     struct iovec iov;
 9     int fput_needed;
10 
11     err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
12     if (unlikely(err))
13         return err;
14     sock = sockfd_lookup_light(fd, &err, &fput_needed);
15     if (!sock)
16         goto out;
17 
18     msg.msg_name = NULL;
19     msg.msg_control = NULL;
20     msg.msg_controllen = 0;
21     msg.msg_namelen = 0;
22     if (addr) {
23         err = move_addr_to_kernel(addr, addr_len, &address);
24         if (err < 0)
25             goto out_put;
26         msg.msg_name = (struct sockaddr *)&address;
27         msg.msg_namelen = addr_len;
28     }
29     if (sock->file->f_flags & O_NONBLOCK)
30         flags |= MSG_DONTWAIT;
31     msg.msg_flags = flags;
32     err = sock_sendmsg(sock, &msg);
33 
34 out_put:
35     fput_light(sock->file, fput_needed);
36 out:
37     return err;
38 }

  __sys_sendto()构建完这些后,调用sock_sendmsg()继续执行发送流程,传入参数为socket和struct msghdr,进行安全检查。

int sock_sendmsg(struct socket *sock, struct msghdr *msg)
{
    int err = security_socket_sendmsg(sock, msg,
                      msg_data_left(msg));

    return err ?: sock_sendmsg_nosec(sock, msg);
}
EXPORT_SYMBOL(sock_sendmsg);

  在发送多个消息时,为了提高性能会调用sock_sendmsg_nosec(),这是为了跳过上面的安全检查,提高性能。简单检查后就进入__sock_sendmsg_nosec()。

1 static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
2 {
3     int ret = INDIRECT_CALL_INET(sock->ops->sendmsg, inet6_sendmsg,
4                      inet_sendmsg, sock, msg,
5                      msg_data_left(msg));
6     BUG_ON(ret == -EIOCBQUEUED);
7     return ret;
8 }

  inet_sendmsg()函数首先检查本地端口是否已绑定,无绑定则执行自动绑定,而后调用具体协议的sendmsg函数。

 1 int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
 2 {
 3     struct sock *sk = sock->sk;
 4 
 5     if (unlikely(inet_send_prepare(sk)))
 6         return -EAGAIN;
 7 
 8     return INDIRECT_CALL_2(sk->sk_prot->sendmsg, tcp_sendmsg, udp_sendmsg,
 9                    sk, msg, size);
10 }
11 EXPORT_SYMBOL(inet_sendmsg);

  tcp_sendmsg()的主要工作是先把sk加锁,之后调用tcp_sendmsg_locked()对用户数据进行处理,再释放锁。

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
    int ret;

    lock_sock(sk);
    ret = tcp_sendmsg_locked(sk, msg, size);
    release_sock(sk);

    return ret;
}
EXPORT_SYMBOL(tcp_sendmsg);

4.2传输层

传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,主要功能包括
  ①构造 TCP segment
  ②计算 checksum
  ③发送回复(ACK)包
  ④滑动窗口(sliding windown)等保证可靠性的操作

TCP 栈简要过程:

  ①tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开始 segement 发送流程。

  ②构造 TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从 userspace buffer 中拷贝 packet 的数据到 skb 的 buffer。

  ③构造 TCP header。

  ④计算 TCP 校验和(checksum)和 顺序号 (sequence number)。

    TCP 校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。TCP校验和覆盖 TCP 首部和 TCP 数据。

    TCP的校验和是必需的。

  ⑤发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程。

gdb调试及源码分析

  tcp_sendmsg_locked()函数主要负责用户数据的存放。之后的发送数据,主要涉及__tcp_push_pending_frames、tcp_push_one、tcp_push这三个函数。实际上tcp_push_one是tcp_push的一种特殊形式,且tcp_push简单封装后也会调用__tcp_push_pending_frames,因此我们主要介绍tcp_push这个发送函数。

 1 int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
 2 {
 3     struct tcp_sock *tp = tcp_sk(sk);
 4     struct ubuf_info *uarg = NULL;
 5     struct sk_buff *skb;
 6     struct sockcm_cookie sockc;
 7     int flags, err, copied = 0;
 8     int mss_now = 0, size_goal, copied_syn = 0;
 9     int process_backlog = 0;
10     bool zc = false;
11     long timeo;
12 
13     flags = msg->msg_flags;
14 
15     if (flags & MSG_ZEROCOPY && size && sock_flag(sk, SOCK_ZEROCOPY)) {
16         skb = tcp_write_queue_tail(sk);
17         uarg = sock_zerocopy_realloc(sk, size, skb_zcopy(skb));
18         if (!uarg) {
19             err = -ENOBUFS;
20             goto out_err;
21         }
22 
23         zc = sk->sk_route_caps & NETIF_F_SG;
24         if (!zc)
25             uarg->zerocopy = 0;
26     }
27     ......
28 }

   在tcp_push()中涉及到小包阻塞的问题,使用了TSQ机制,即TCP Small Queue,通过tcp_should_autocork()判断是否开启。其基本思想就是利用数据报文发送的这段时间,将小包尽量组合成大包发送,既减小发送带宽,同时也不会降低传输速率(网卡和发送队列有报文发送时才阻塞,无报文等待发送时,就直接发送,不阻塞)。

 1 static void tcp_push(struct sock *sk, int flags, int mss_now,
 2              int nonagle, int size_goal)
 3 {
 4     struct tcp_sock *tp = tcp_sk(sk);
 5     struct sk_buff *skb;
 6 
 7     skb = tcp_write_queue_tail(sk);
 8     if (!skb)
 9         return;
10     if (!(flags & MSG_MORE) || forced_push(tp))
11         tcp_mark_push(tp, skb);
12 
13     tcp_mark_urg(tp, flags);
14 
15     if (tcp_should_autocork(sk, skb, size_goal)) {
16 
17         /* avoid atomic op if TSQ_THROTTLED bit is already set */
18         if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) {
19             NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);
20             set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags);
21         }
22         /* It is possible TX completion already happened
23          * before we set TSQ_THROTTLED.
24          */
25         if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize)
26             return;
27     }
28 
29     if (flags & MSG_MORE)
30         nonagle = TCP_NAGLE_CORK;
31 
32     __tcp_push_pending_frames(sk, mss_now, nonagle);
33 }

   如果没有阻塞,则调用__tcp_push_pending_frames()继续往下递交数据发送。

 1 void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
 2                    int nonagle)
 3 {
 4     /* If we are closed, the bytes will have to remain here.
 5      * In time closedown will finish, we empty the write queue and
 6      * all will be happy.
 7      */
 8     if (unlikely(sk->sk_state == TCP_CLOSE))
 9         return;
10 
11     if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
12                sk_gfp_mask(sk, GFP_ATOMIC)))
13         tcp_check_probe_timer(sk);
14 }

   上面有讲到使用TSQ机制判断是否要阻塞小包,进入tcp_write_xmit()后还会使用nagle算法判断是否 要阻塞发送。

 1 tatic bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
 2                int push_one, gfp_t gfp)
 3 {
 4     struct tcp_sock *tp = tcp_sk(sk);
 5     struct sk_buff *skb;
 6     unsigned int tso_segs, sent_pkts;
 7     int cwnd_quota;
 8     int result;
 9     bool is_cwnd_limited = false, is_rwnd_limited = false;
10     u32 max_segs;
11 
12     sent_pkts = 0;
13 
14     tcp_mstamp_refresh(tp);
15     if (!push_one) {
16         /* Do MTU probing. */
17         result = tcp_mtu_probe(sk);
18         if (!result) {
19             return false;
20         } else if (result > 0) {
21             sent_pkts = 1;
22         }
23     }
24     ......
25 }

   一切顺利的情况下,最后将发往tcp_transmit_skb()去填充TCP头部,然后交往IP层继续封装。

 1 static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
 2                   int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
 3 {
 4     const struct inet_connection_sock *icsk = inet_csk(sk);
 5     struct inet_sock *inet;
 6     struct tcp_sock *tp;
 7     struct tcp_skb_cb *tcb;
 8     struct tcp_out_options opts;
 9     unsigned int tcp_options_size, tcp_header_size;
10     struct sk_buff *oskb = NULL;
11     struct tcp_md5sig_key *md5;
12     struct tcphdr *th;
13     u64 prior_wstamp;
14     int err;
15 
16     BUG_ON(!skb || !tcp_skb_pcount(skb));
17     tp = tcp_sk(sk);
18     prior_wstamp = tp->tcp_wstamp_ns;
19     tp->tcp_wstamp_ns = max(tp->tcp_wstamp_ns, tp->tcp_clock_cache);
20     skb->skb_mstamp_ns = tp->tcp_wstamp_ns;
21     if (clone_it) {
22         TCP_SKB_CB(skb)->tx.in_flight = TCP_SKB_CB(skb)->end_seq
23             - tp->snd_una;
24         oskb = skb;
25 
26         tcp_skb_tsorted_save(oskb) {
27             if (unlikely(skb_cloned(oskb)))
28                 skb = pskb_copy(oskb, gfp_mask);
29             else
30                 skb = skb_clone(oskb, gfp_mask);
31         } tcp_skb_tsorted_restore(oskb);
32 
33         if (unlikely(!skb))
34             return -ENOBUFS;
35         /* retransmit skbs might have a non zero value in skb->dev
36          * because skb->dev is aliased with skb->rbnode.rb_left
37          */
38         skb->dev = NULL;
39     }
40     ......
41 }

 4.3网络层

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

  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 获取。

gdb调试及源码分析

  ip_queue_xmit()是网络层的入口函数。

1 static inline int ip_queue_xmit(struct sock *sk, struct sk_buff *skb,
2                 struct flowi *fl)
3 {
4     return __ip_queue_xmit(sk, skb, fl, inet_sk(sk)->tos);
5 }

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

 1 int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl,
 2             __u8 tos)
 3 {
 4     struct inet_sock *inet = inet_sk(sk);
 5     struct net *net = sock_net(sk);
 6     struct ip_options_rcu *inet_opt;
 7     struct flowi4 *fl4;
 8     struct rtable *rt;
 9     struct iphdr *iph;
10     int res;
11 
12     /* Skip all of this if the packet is already routed,
13      * f.e. by something like SCTP.
14      */
15     rcu_read_lock();
16     inet_opt = rcu_dereference(inet->inet_opt);
17     fl4 = &fl->u.ip4;
18     rt = skb_rtable(skb);
19     if (rt)
20         goto packet_routed;
21 
22     /* Make sure we can route this packet. */
23     rt = (struct rtable *)__sk_dst_check(sk, 0);
24     if (!rt) {
25         __be32 daddr;
26 
27         /* Use correct destination address if we have options. */
28         daddr = inet->inet_daddr;
29         if (inet_opt && inet_opt->opt.srr)
30             daddr = inet_opt->opt.faddr;
31 
32         /* If this fails, retransmit mechanism of transport layer will
33          * keep trying until route appears or the connection times
34          * itself out.
35          */
36         rt = ip_route_output_ports(net, fl4, sk,
37                        daddr, inet->inet_saddr,
38                        inet->inet_dport,
39                        inet->inet_sport,
40                        sk->sk_protocol,
41                        RT_CONN_FLAGS_TOS(sk, tos),
42                        sk->sk_bound_dev_if);
43         if (IS_ERR(rt))
44             goto no_route;
45         sk_setup_caps(sk, &rt->dst);
46     }
47     skb_dst_set_noref(skb, &rt->dst);
48 
49 packet_routed:
50     if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway)
51         goto no_route;
52 
53     /* OK, we know where to send it, allocate and build IP header. */
54     skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
55     skb_reset_network_header(skb);
56     iph = ip_hdr(skb);
57     *((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (tos & 0xff));
58     if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)
59         iph->frag_off = htons(IP_DF);
60     else
61         iph->frag_off = 0;
62     iph->ttl      = ip_select_ttl(inet, &rt->dst);
63     iph->protocol = sk->sk_protocol;
64     ip_copy_addrs(iph, fl4);
65 
66     /* Transport layer set skb->h.foo itself. */
67 
68     if (inet_opt && inet_opt->opt.optlen) {
69         iph->ihl += inet_opt->opt.optlen >> 2;
70         ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0);
71     }
72 
73     ip_select_ident_segs(net, skb, sk,
74                  skb_shinfo(skb)->gso_segs ?: 1);
75 
76     /* TODO : should we use skb->sk here instead of sk ? */
77     skb->priority = sk->sk_priority;
78     skb->mark = sk->sk_mark;
79 
80     res = ip_local_out(net, sk, skb);
81     rcu_read_unlock();
82     return res;
83 
84 no_route:
85     rcu_read_unlock();
86     IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
87     kfree_skb(skb);
88     return -EHOSTUNREACH;
89 }
90 EXPORT_SYMBOL(__ip_queue_xmit);

  ip_output()函数设置输出设备和协议,然后经过钩子,最后调用ip_finish_output()。

 1 int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
 2 {
 3     struct net_device *dev = skb_dst(skb)->dev;
 4 
 5     IP_UPD_PO_STATS(net, IPSTATS_MIB_OUT, skb->len);
 6 
 7     skb->dev = dev;
 8     skb->protocol = htons(ETH_P_IP);
 9 
10     return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
11                 net, sk, skb, NULL, dev,
12                 ip_finish_output,
13                 !(IPCB(skb)->flags & IPSKB_REROUTED));
14 }

   ip_output()中调用NF_HOOK_COND()函数。宏NF_HOOK是实现netfilter挂接点的,在桥代码打入内核后,又添加了两个挂接点:NF_HOOK_COND和NF_HOOK_THRESH。

 1 static inline int
 2 NF_HOOK_COND(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk,
 3          struct sk_buff *skb, struct net_device *in, struct net_device *out,
 4          int (*okfn)(struct net *, struct sock *, struct sk_buff *),
 5          bool cond)
 6 {
 7     int ret;
 8 
 9     if (!cond ||
10         ((ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn)) == 1))
11         ret = okfn(net, sk, skb);
12     return ret;
13 }

   ip_finish_output()函数对skb进行分片判断,需要分片,则分片后输出,不需要分片则知直接输出。

 1 static int ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
 2 {
 3     int ret;
 4 
 5     ret = BPF_CGROUP_RUN_PROG_INET_EGRESS(sk, skb);
 6     switch (ret) {
 7     case NET_XMIT_SUCCESS:
 8         return __ip_finish_output(net, sk, skb);
 9     case NET_XMIT_CN:
10         return __ip_finish_output(net, sk, skb) ? : ret;
11     default:
12         kfree_skb(skb);
13         return ret;
14     }
15 }

   ip_finish_output2()对skb的头部空间进行检查,看是否能够容纳下二层头部,若空间不足,则需要重新申请skb;然后,获取邻居子系统,并通过邻居子系统输出。

 1 static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
 2 {
 3     struct dst_entry *dst = skb_dst(skb);
 4     struct rtable *rt = (struct rtable *)dst;
 5     struct net_device *dev = dst->dev;
 6     unsigned int hh_len = LL_RESERVED_SPACE(dev);
 7     struct neighbour *neigh;
 8     bool is_v6gw = false;
 9 
10     if (rt->rt_type == RTN_MULTICAST) {
11         IP_UPD_PO_STATS(net, IPSTATS_MIB_OUTMCAST, skb->len);
12     } else if (rt->rt_type == RTN_BROADCAST)
13         IP_UPD_PO_STATS(net, IPSTATS_MIB_OUTBCAST, skb->len);
14 
15     /* Be paranoid, rather than too clever. */
16     if (unlikely(skb_headroom(skb) < hh_len && dev->header_ops)) {
17         struct sk_buff *skb2;
18 
19         skb2 = skb_realloc_headroom(skb, LL_RESERVED_SPACE(dev));
20         if (!skb2) {
21             kfree_skb(skb);
22             return -ENOMEM;
23         }
24         if (skb->sk)
25             skb_set_owner_w(skb2, skb->sk);
26         consume_skb(skb);
27         skb = skb2;
28     }
29 
30     if (lwtunnel_xmit_redirect(dst->lwtstate)) {
31         int res = lwtunnel_xmit(skb);
32 
33         if (res < 0 || res == LWTUNNEL_XMIT_DONE)
34             return res;
35     }
36 
37     rcu_read_lock_bh();
38     neigh = ip_neigh_for_gw(rt, skb, &is_v6gw);
39     if (!IS_ERR(neigh)) {
40         int res;
41 
42         sock_confirm_neigh(skb, neigh);
43         /* if crossing protocols, can not use the cached header */
44         res = neigh_output(neigh, skb, is_v6gw);
45         rcu_read_unlock_bh();
46         return res;
47     }
48     rcu_read_unlock_bh();
49 
50     net_dbg_ratelimited("%s: No header cache and no neighbour!\n",
51                 __func__);
52     kfree_skb(skb);
53     return -EINVAL;
54 }

   ip层在构造好ip头,检查完分片之后,会调用邻居子系统的输出函数neigh_output进行输出,输出分为有二层头缓存和没有两种情况,有缓存时调用neigh_hh_output进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出。

 1 static inline int neigh_output(struct neighbour *n, struct sk_buff *skb,
 2                    bool skip_cache)
 3 {
 4     const struct hh_cache *hh = &n->hh;
 5 
 6     if ((n->nud_state & NUD_CONNECTED) && hh->hh_len && !skip_cache)
 7         return neigh_hh_output(hh, skb);
 8     else
 9         return n->output(n, skb);
10 }

 4.4数据链路层

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

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

gdb调试及源码分析

  当上层应用试图建立一个TCP的链接,或者发送一个封包的时候,在kernel的协议栈部分,在TCP/UDP层会组成一个网络的封包,然后通过IP进行路由选择以及iptables的Hook,之后 到neighbor层查询或者询问下一跳的链路层地址,然后通过调用dev_queue_xmit这个网络设备接口层函数发送给driver。

  dev_queue_xmit函数实际上调用的是__dev_queue_xmit函数。对_dev_queue_xmit函数的分析来看,发送报文有2中情况:

  ①有拥塞控制策略的情况,比较复杂,但是目前最常用

  ②没有enqueue的状况,比较简单,直接发送到driver,如loopback等使用

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

 1 static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
 2 {
 3     struct net_device *dev = skb->dev;
 4     struct netdev_queue *txq;
 5     struct Qdisc *q;
 6     int rc = -ENOMEM;
 7     bool again = false;
 8 
 9     skb_reset_mac_header(skb);
10 
11     if (unlikely(skb_shinfo(skb)->tx_flags & SKBTX_SCHED_TSTAMP))
12         __skb_tstamp_tx(skb, NULL, skb->sk, SCM_TSTAMP_SCHED);
13 
14     /* Disable soft irqs for various locks below. Also
15      * stops preemption for RCU.
16      */
17     rcu_read_lock_bh();
18 
19     skb_update_prio(skb);
20 
21     qdisc_pkt_len_init(skb);
22     ......
23 }

  dev_hard_start_xmit()主要任务是发送一个到多个数据包。

 1 struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,
 2                     struct netdev_queue *txq, int *ret)
 3 {
 4     struct sk_buff *skb = first;
 5     int rc = NETDEV_TX_OK;
 6 
 7     while (skb) {
 8         struct sk_buff *next = skb->next;
 9 
10         skb_mark_not_on_list(skb);
11         rc = xmit_one(skb, dev, txq, next != NULL);
12         if (unlikely(!dev_xmit_complete(rc))) {
13             skb->next = next;
14             goto out;
15         }
16 
17         skb = next;
18         if (netif_tx_queue_stopped(txq) && skb) {
19             rc = NETDEV_TX_BUSY;
20             break;
21         }
22     }
23 
24 out:
25     *ret = rc;
26     return skb;
27 }

   xmit_one, netdev_start_xmit,__netdev_start_xmit 这个三个函数,其目的就是将封包送到driver的tx函数,中间在送往driver之前,还会经历抓包的过程。

 1 static int xmit_one(struct sk_buff *skb, struct net_device *dev,
 2             struct netdev_queue *txq, bool more)
 3 {
 4     unsigned int len;
 5     int rc;
 6 
 7     if (dev_nit_active(dev))
 8         dev_queue_xmit_nit(skb, dev);
 9 
10     len = skb->len;
11     trace_net_dev_start_xmit(skb, dev);
12     rc = netdev_start_xmit(skb, dev, txq, more);
13     trace_net_dev_xmit(skb, rc, dev, len);
14 
15     return rc;
16 }

 4.5物理层

  物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中。在数据拷贝中,同时加入符合以太网协议的相关header,IFG、前导符和CRC。对于以太网网络,物理层发送采用CSMA/CD,即在发送过程中侦听链路冲突。

  一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。

 

5.recv过程

5.1应用层

  ①每当用户应用调用 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。

gdb调试及源码分析

  根据调用栈和函数调用顺序,查看以下源码:

 1 SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
 2         unsigned int, flags, struct sockaddr __user *, addr,
 3         int __user *, addr_len)
 4 {
 5     return __sys_recvfrom(fd, ubuf, size, flags, addr, addr_len);
 6 }
 7 
 8 SYSCALL_DEFINE4(recv, int, fd, void __user *, ubuf, size_t, size,
 9         unsigned int, flags)
10 {
11     return __sys_recvfrom(fd, ubuf, size, flags, NULL, NULL);
12 }

  在内核的实现中,recv和recvfrom系统调用最终都会调用到内核函数__sys_recvfrom()。

 1 int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
 2            struct sockaddr __user *addr, int __user *addr_len)
 3 {
 4     struct socket *sock;
 5     struct iovec iov;
 6     struct msghdr msg;
 7     struct sockaddr_storage address;
 8     int err, err2;
 9     int fput_needed;
10 
11     err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
12     if (unlikely(err))
13         return err;
14     sock = sockfd_lookup_light(fd, &err, &fput_needed);
15     if (!sock)
16         goto out;
17 
18     msg.msg_control = NULL;
19     msg.msg_controllen = 0;
20     /* Save some cycles and don't copy the address if not needed */
21     msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
22     /* We assume all kernel code knows the size of sockaddr_storage */
23     msg.msg_namelen = 0;
24     msg.msg_iocb = NULL;
25     msg.msg_flags = 0;
26     if (sock->file->f_flags & O_NONBLOCK)
27         flags |= MSG_DONTWAIT;
28     err = sock_recvmsg(sock, &msg, flags);
29 
30     if (err >= 0 && addr != NULL) {
31         err2 = move_addr_to_user(&address,
32                      msg.msg_namelen, addr, addr_len);
33         if (err2 < 0)
34             err = err2;
35     }
36 
37     fput_light(sock->file, fput_needed);
38 out:
39     return err;
40 }

  sock_recvmsg()函数

1 int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags)
2 {
3     int err = security_socket_recvmsg(sock, msg, msg_data_left(msg), flags);
4 
5     return err ?: sock_recvmsg_nosec(sock, msg, flags);
6 }
7 EXPORT_SYMBOL(sock_recvmsg);

  sock_recvmsg_nosec()函数

1 static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg,
2                      int flags)
3 {
4     return INDIRECT_CALL_INET(sock->ops->recvmsg, inet6_recvmsg,
5                   inet_recvmsg, sock, msg, msg_data_left(msg),
6                   flags);
7 }

  inet_recvmsg()函数

 1 int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
 2          int flags)
 3 {
 4     struct sock *sk = sock->sk;
 5     int addr_len = 0;
 6     int err;
 7 
 8     if (likely(!(flags & MSG_ERRQUEUE)))
 9         sock_rps_record_flow(sk);
10 
11     err = INDIRECT_CALL_2(sk->sk_prot->recvmsg, tcp_recvmsg, udp_recvmsg,
12                   sk, msg, size, flags & MSG_DONTWAIT,
13                   flags & ~MSG_DONTWAIT, &addr_len);
14     if (err >= 0)
15         msg->msg_namelen = addr_len;
16     return err;
17 }

   最后调用tcp_recvmsg()。

5.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。

gdb调试及源码分析

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

 1 int tcp_v4_rcv(struct sk_buff *skb)
 2 {
 3     struct net *net = dev_net(skb->dev);
 4     struct sk_buff *skb_to_free;
 5     int sdif = inet_sdif(skb);
 6     const struct iphdr *iph;
 7     const struct tcphdr *th;
 8     bool refcounted;
 9     struct sock *sk;
10     int ret;
11 
12     if (skb->pkt_type != PACKET_HOST)
13         goto discard_it;
14 
15     /* Count it even if it's bad */
16     __TCP_INC_STATS(net, TCP_MIB_INSEGS);
17 
18     if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
19         goto discard_it;
20 
21     th = (const struct tcphdr *)skb->data;
22 
23     if (unlikely(th->doff < sizeof(struct tcphdr) / 4))
24         goto bad_packet;
25     if (!pskb_may_pull(skb, th->doff * 4))
26         goto discard_it;
27     
28     ......
29 }

  tcp_v4_do_rcv调用tcp_rcv_established函数将SKB加入sk-> receive_queue中,检查TCP数据包校验和,如果sk的状态为TCP_LISTEN,则调用tcp_v4_hnd_req函数生成一个新的sock用于传输,之后处理sk的状态转换。

 1 int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
 2 {
 3     struct sock *rsk;
 4 
 5     if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
 6         struct dst_entry *dst = sk->sk_rx_dst;
 7 
 8         sock_rps_save_rxhash(sk, skb);
 9         sk_mark_napi_id(sk, skb);
10         if (dst) {
11             if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif ||
12                 !dst->ops->check(dst, 0)) {
13                 dst_release(dst);
14                 sk->sk_rx_dst = NULL;
15             }
16         }
17         tcp_rcv_established(sk, skb);
18         return 0;
19     }
20 
21     if (tcp_checksum_complete(skb))
22         goto csum_err;
23 
24     if (sk->sk_state == TCP_LISTEN) {
25         struct sock *nsk = tcp_v4_cookie_check(sk, skb);
26 
27         if (!nsk)
28             goto discard;
29         if (nsk != sk) {
30             if (tcp_child_process(sk, nsk, skb)) {
31                 rsk = nsk;
32                 goto reset;
33             }
34             return 0;
35         }
36     } else
37         sock_rps_save_rxhash(sk, skb);
38 
39     if (tcp_rcv_state_process(sk, skb)) {
40         rsk = sk;
41         goto reset;
42     }
43     return 0;
44 
45 reset:
46     tcp_v4_send_reset(rsk, skb);
47 discard:
48     kfree_skb(skb);
49     /* Be careful here. If this function gets more complicated and
50      * gcc suffers from register pressure on the x86, sk (in %ebx)
51      * might be destroyed here. This current version compiles correctly,
52      * but you have been warned.
53      */
54     return 0;
55 
56 csum_err:
57     TCP_INC_STATS(sock_net(sk), TCP_MIB_CSUMERRORS);
58     TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS);
59     goto discard;
60 }
61 EXPORT_SYMBOL(tcp_v4_do_rcv);

  tcp_rcv_established用于处理已连接状态下的输入,处理过程根据首部预测字段分为快速路径和慢速路径:在快路中,对是有有数据负荷进行不同处理:

  (1) 若无数据,则处理输入ack,释放该skb,检查是否有数据发送,有则发送;

  (2) 若有数据,检查是否当前处理进程上下文,并且是期望读取的数据,若是则将数据复制到用户空间,若不满足直接复制到用户空间的情况,或者复制失败,则需要将数据段加入到接收队列中,加入方式包括合并到已有数据段,或者加入队列尾部,并唤醒用户进程通知有数据可读;

在慢路中,会进行更详细的校验,然后处理ack,处理紧急数据,接收数据段,其中数据段可能包含乱序的情况,最后进行是否有数据和ack的发送检查。

 1 void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
 2 {
 3     const struct tcphdr *th = (const struct tcphdr *)skb->data;
 4     struct tcp_sock *tp = tcp_sk(sk);
 5     unsigned int len = skb->len;
 6 
 7     /* TCP congestion window tracking */
 8     trace_tcp_probe(sk, skb);
 9 
10     tcp_mstamp_refresh(tp);
11     if (unlikely(!sk->sk_rx_dst))
12         inet_csk(sk)->icsk_af_ops->sk_rx_dst_set(sk, skb);
13 
14     tp->rx_opt.saw_tstamp = 0;
15     
16     ......
17 }

  tcp_rcv_established的慢路径中调用tcp_data_queue,tcp_data_queue作用为数据段的接收处理,其中分为多种情况:

  ①无数据,释放skb,返回;

  ②预期接收的数据段,a. 进行0窗口判断;b. 进程上下文,复制数据到用户空间;c. 不满足b或者b未完整拷贝此skb的数据段,则加入到接收队列;d. 更新下一个期望接收的序号;e. 若有fin标记,则处理fin;f. 乱序队列不为空,则处理乱序;g. 快速路径的检查和设置;h. 唤醒用户空间进程读取数据;

  ③重传的数据段,进入快速ack模式,释放该skb;

  ④ 窗口以外的数据段,进入快速ack模式,释放该skb;

  ⑤数据段重叠,在进行0窗口判断之后,进行(2)中的加入接收队列,以及>=d的流程;

  ⑥ 乱序的数据段,调用tcp_data_queue_ofo进行乱序数据段的接收处理

 1 static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
 2 {
 3     struct tcp_sock *tp = tcp_sk(sk);
 4     bool fragstolen;
 5     int eaten;
 6 
 7     if (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq) {
 8         __kfree_skb(skb);
 9         return;
10     }
11     skb_dst_drop(skb);
12     __skb_pull(skb, tcp_hdr(skb)->doff * 4);
13 
14     tcp_ecn_accept_cwr(sk, skb);
15 
16     tp->rx_opt.dsack = 0;
17 
18     /*  Queue data for delivery to the user.
19      *  Packets in sequence go to the receive queue.
20      *  Out of sequence packets to the out_of_order_queue.
21      */
22     if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
23         if (tcp_receive_window(tp) == 0) {
24             NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPZEROWINDOWDROP);
25             goto out_of_window;
26         }
27     
28     ......
29 }

 5.3网络层

简要过程:

  ①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,进入链路层处理流程。

gdb调试及源码分析

  IP 层的入口函数在 ip_rcv 函数,ip_rcv完成基本的校验和处理工作后,经过PRE_ROUTING钩子点。

 1 int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
 2        struct net_device *orig_dev)
 3 {
 4     struct net *net = dev_net(dev);
 5 
 6     skb = ip_rcv_core(skb, net);
 7     if (skb == NULL)
 8         return NET_RX_DROP;
 9 
10     return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
11                net, NULL, skb, dev, NULL,
12                ip_rcv_finish);
13 }

  经过PRE_ROUTING钩子点之后,调用ip_rcv_finish完成数据包接收,包括选项处理,路由查询,并且根据路由决定数据包是发往本机还是转发。

 1 static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
 2 {
 3     struct net_device *dev = skb->dev;
 4     int ret;
 5 
 6     /* if ingress device is enslaved to an L3 master device pass the
 7      * skb to its handler for processing
 8      */
 9     skb = l3mdev_ip_rcv(skb);
10     if (!skb)
11         return NET_RX_SUCCESS;
12 
13     ret = ip_rcv_finish_core(net, sk, skb, dev);
14     if (ret != NET_RX_DROP)
15         ret = dst_input(skb);
16     return ret;
17 }

  ip_rcv_finish在最后调用dst_input。

1 static inline int dst_input(struct sk_buff *skb)
2 {
3     return skb_dst(skb)->input(skb);
4 }

  对于收到的IP报文需要传递给更上层的协议去处理,但是如果收到的是IP分片的那么就需要在往上层传递之前先进行重组,这些就是在ip_local_deliver()函数里面进行的。首先使用ip_hdr()获取ip头部, 然后使用ip_is_fragment()来判断当前IP报文是否是一个分组。如果判断是一个分组的话那么就会调用ip_defrag()进行重组,重组成功就直接返回了,不会调用到ip_local_deliver_finish()。当最后一个报文过来或者是非分组的报文过来的话,ip_is_fragment()判断不通过直接就会进入ip_local_deliver_finish()。

 1 int ip_local_deliver(struct sk_buff *skb)
 2 {
 3     /*
 4      *    Reassemble IP fragments.
 5      */
 6     struct net *net = dev_net(skb->dev);
 7 
 8     if (ip_is_fragment(ip_hdr(skb))) {
 9         if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
10             return 0;
11     }
12 
13     return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
14                net, NULL, skb, skb->dev, NULL,
15                ip_local_deliver_finish);
16 }

  ip_local_deliver_finish函数处理原始套接字的数据接收,并调用上层协议的包接收函数,将数据包传递到传输层。

 1 static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
 2 {
 3      __skb_pull(skb, skb_network_header_len(skb));
 4  
 5      rcu_read_lock();
 6      ip_protocol_deliver_rcu(net, skb, ip_hdr(skb)->protocol);
 7      rcu_read_unlock();
 8  
 9      return 0;
10 }

  ip_local_deliver_finish函数中调用ip_protocol_deliver_rcu函数。

 1 void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol)
 2 {
 3     const struct net_protocol *ipprot;
 4     int raw, ret;
 5 
 6 resubmit:
 7     raw = raw_local_deliver(skb, protocol);
 8 
 9     ipprot = rcu_dereference(inet_protos[protocol]);
10     if (ipprot) {
11         if (!ipprot->no_policy) {
12             if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
13                 kfree_skb(skb);
14                 return;
15             }
16             nf_reset_ct(skb);
17         }
18         ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv,
19                       skb);
20         if (ret < 0) {
21             protocol = -ret;
22             goto resubmit;
23         }
24         __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
25     } else {
26         if (!raw) {
27             if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
28                 __IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS);
29                 icmp_send(skb, ICMP_DEST_UNREACH,
30                       ICMP_PROT_UNREACH, 0);
31             }
32             kfree_skb(skb);
33         } else {
34             __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
35             consume_skb(skb);
36         }
37     }
38 }

5.4数据链路层

简要过程:

  ①一个 package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。

  ②网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中;从数据帧中提取出一些信息,并设置 skb_buff 相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;

  ③终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。

  ④内核 2.5 中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:(1) 通过以前的函数netif_rx;(2)通过NAPI机制。该中断处理程序调用 Network device的 netif_rx_schedule 函数,进入软中断处理流程,再调用 net_rx_action 函数。

  ⑤该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从 rx_ring 中被删除,进入 netif _receive_skb 处理流程。

  ⑥netif_receive_skb 是链路层接收数据报的最后一站。它根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。

gdb调试及源码分析

net_rx_action函数的执行步骤如下:

  ①回收当前处理器的poll_list链表的引用。

  ②将jiffies的值保存在start_time变量中。

  ③设置轮询的budget(预算,可处理的数据包数量)为netdev_budget变量的初始值(这个值可以通过 /proc/sys/net/core/netdev_budget来配置)

  ④轮询poll_list链表中的每个设备,直到你的budget用完,当你的运行时间还没有超过一个jiffies时:

    a) 如果quantum(配额)为正值则调用设备的poll()函数,否则将weight的值加到quantum中,将设备放回poll_list链表;

    a.1) 如果poll()函数返回一个非零值,将weight的值设置到quantum中然后将设备放回poll_list链表;

    a.2) 如果poll()函数返回零值,说明设备已经被移除poll_list链表(不再处于轮询状态)。

 1 static __latent_entropy void net_rx_action(struct softirq_action *h)
 2 {
 3     struct softnet_data *sd = this_cpu_ptr(&softnet_data);
 4     unsigned long time_limit = jiffies +
 5         usecs_to_jiffies(netdev_budget_usecs);
 6     int budget = netdev_budget;
 7     LIST_HEAD(list);
 8     LIST_HEAD(repoll);
 9 
10     local_irq_disable();
11     list_splice_init(&sd->poll_list, &list);
12     local_irq_enable();
13 
14     for (;;) {
15         struct napi_struct *n;
16 
17         if (list_empty(&list)) {
18             if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))
19                 goto out;
20             break;
21         }
22 
23         n = list_first_entry(&list, struct napi_struct, poll_list);
24         budget -= napi_poll(n, &repoll);
25 
26         /* If softirq window is exhausted then punt.
27          * Allow this to run for 2 jiffies since which will allow
28          * an average latency of 1.5/HZ.
29          */
30         if (unlikely(budget <= 0 ||
31                  time_after_eq(jiffies, time_limit))) {
32             sd->time_squeeze++;
33             break;
34         }
35     }
36 
37     local_irq_disable();
38 
39     list_splice_tail_init(&sd->poll_list, &list);
40     list_splice_tail(&repoll, &list);
41     list_splice(&list, &sd->poll_list);
42     if (!list_empty(&sd->poll_list))
43         __raise_softirq_irqoff(NET_RX_SOFTIRQ);
44 
45     net_rps_action_and_irq_enable(sd);
46 out:
47     __kfree_skb_flush();
48 }

  napi_poll是被软中断处理函数net_rx_action调用的。这个函数将在napi_struct.weight规定的时间内,被net_rx_action循环调用,直到时间片用尽或者网卡当前DMA中所有缓存的数据包被处理完。如果是由于时间片用尽而退出的的话,napi_struct会重新挂载到softnet_data上,而如果是所有数据包处理完退出的,napi_struct会从softnet_data上移除并重新打开网卡硬件中断。

 1 static int napi_poll(struct napi_struct *n, struct list_head *repoll)
 2 {
 3     void *have;
 4     int work, weight;
 5 
 6     list_del_init(&n->poll_list);
 7 
 8     have = netpoll_poll_lock(n);
 9 
10     weight = n->weight;
11 
12     /* This NAPI_STATE_SCHED test is for avoiding a race
13      * with netpoll's poll_napi().  Only the entity which
14      * obtains the lock and sees NAPI_STATE_SCHED set will
15      * actually make the ->poll() call.  Therefore we avoid
16      * accidentally calling ->poll() when NAPI is not scheduled.
17      */
18     work = 0;
19     if (test_bit(NAPI_STATE_SCHED, &n->state)) {
20         work = n->poll(n, weight);
21         trace_napi_poll(n, work, weight);
22     }
23 
24     WARN_ON_ONCE(work > weight);
25 
26     if (likely(work < weight))
27         goto out_unlock;
28 
29     /* Drivers must not modify the NAPI state if they
30      * consume the entire weight.  In such cases this code
31      * still "owns" the NAPI instance and therefore can
32      * move the instance around on the list at-will.
33      */
34     if (unlikely(napi_disable_pending(n))) {
35         napi_complete(n);
36         goto out_unlock;
37     }
38 
39     if (n->gro_bitmask) {
40         /* flush too old packets
41          * If HZ < 1000, flush all packets.
42          */
43         napi_gro_flush(n, HZ >= 1000);
44     }
45 
46     gro_normal_list(n);
47 
48     /* Some drivers may have called napi_schedule
49      * prior to exhausting their budget.
50      */
51     if (unlikely(!list_empty(&n->poll_list))) {
52         pr_warn_once("%s: Budget exhausted after napi rescheduled\n",
53                  n->dev ? n->dev->name : "backlog");
54         goto out_unlock;
55     }
56 
57     list_add_tail(&n->poll_list, repoll);
58 
59 out_unlock:
60     netpoll_poll_unlock(have);
61 
62     return work;
63 }

  __netif_receive_skb_core函数主要有几个处理:

  ①vlan报文的处理,主要是循环把vlan头剥掉,如果qinq场景,两个vlan都会被剥掉;

  ②交给rx_handler处理,例如OVS、linux bridge等;

  ③ptype_all处理,例如抓包程序、raw socket等;

  ④ptype_base处理,交给协议栈处理,例如ip、arp、rarp等;

 1 static int __netif_receive_skb_one_core(struct sk_buff *skb, bool pfmemalloc)
 2 {
 3     struct net_device *orig_dev = skb->dev;
 4     struct packet_type *pt_prev = NULL;
 5     int ret;
 6 
 7     ret = __netif_receive_skb_core(skb, pfmemalloc, &pt_prev);
 8     if (pt_prev)
 9         ret = INDIRECT_CALL_INET(pt_prev->func, ipv6_rcv, ip_rcv, skb,
10                      skb->dev, pt_prev, orig_dev);
11     return ret;
12 }

 

6.时序图

 

posted @ 2021-01-21 09:53  闫东辉  阅读(421)  评论(0编辑  收藏  举报