Linux 发送网络数据包

1 Linux 网络发送过程总览

简单阐述下 send 发送了的数据是如何一步一步被发送到网卡的。

image.png

用户数据被拷贝到内核态,然后经过协议栈处理后进入到了 RingBuffer 中。随后网卡驱动真正将数据发送了出去。当发送完成的时候,是通过硬中断来通知 CPU,然后清理 RingBuffer。

image.png

释放缓存队列等内存。

image.png

2 网卡启动准备

现在的服务器上的网卡一般都是支持多队列的。每一个队列上都是由一个 RingBuffer 表示的,开启了多队列以后的的网卡就会对应有多个 RingBuffer。

image.png

注意:RingBUffer 是在内存上的,内核和网卡会形成一种类似MMAP的映射关系

将来在发送的时候,这两个环形数组中相同位置的指针将都将指向同一个 skb。这样,内核和硬件就能共同访问同样的数据了,内核往 skb 里写数据,网卡硬件负责发送。

image.png

3 accept 创建新 socket

在发送数据之前,我们往往还需要一个已经建立好连接的 socket。

我们就以开篇服务器缩微源代码中提到的 accept 为例,当 accept 之后,进程会创建一个新的 socket 出来,然后把它放到当前进程的打开文件列表中,专门用于和对应的客户端通信。

假设服务器进程通过 accept 和客户端建立了两条连接,我们来简单看一下这两条连接和进程的关联关系。

image.png

其中代表一条连接的 socket 内核对象更为具体一点的结构图如下。

image.png

可以发现到上一篇linux接受数据包中所说的接受队列

4 发送数据真正开始

4.1 send 系统调用实现

send 系统调用的源码位于文件 net/socket.c 中。在这个系统调用里,内部其实真正使用的是 sendto 系统调用。整个调用链条虽然不短,但其实主要只干了两件简单的事情,

第一是在内核中把真正的 socket 找出来,在这个对象里记录着各种协议栈的函数地址。
第二是构造一个 struct msghdr 对象,把用户传入的数据,比如 buffer地址、数据长度啥的,统统都装进去.
剩下的事情就交给下一层,协议栈里的函数 inet_sendmsg 了,其中 inet_sendmsg 函数的地址是通过 socket 内核对象里的 ops 成员找到的。大致流程如图。

image.png

我们在用户态使用的 send 函数和 sendto 函数其实都是 sendto 系统调用实现的。send 只是为了方便,封装出来的一个更易于调用的方式而已。

在 sendto 系统调用里,首先根据用户传进来的 socket 句柄号来查找真正的 socket 内核对象。接着把用户请求的 buff、len、flag 等参数都统统打包到一个 struct msghdr 对象中。

4.2 传输层处理

1)传输层拷贝

在进入到协议栈 inet_sendmsg 以后,内核接着会找到 socket 上的具体协议发送函数。对于 TCP 协议来说,那就是 tcp_sendmsg(同样也是通过 socket 内核对象找到的)。

在这个函数中,内核会申请一个内核态的 skb 内存,将用户待发送的数据拷贝进去注意这个时候不一定会真正开始发送,如果没有达到发送条件的话很可能这次调用直接就返回

注意下:这里会将 用户态内存要发送的数据拷贝到内核态的 skb 里面,涉及到一次或者几次内存拷贝的开销。

image.png

image.png

到这里,用户线程就可以返回呢,而内核什么时候发送数据,需要由内核进行进一步的判断。

2)传输层发送

image.png

假设现在内核发送条件已经满足了,内核会调用 tcp_write_xmit。

所以我们直接从 tcp_write_xmit 看起,这个函数处理了传输层的拥塞控制、滑动窗口相关的工作。满足窗口要求的时候,设置一下 TCP 头然后将 skb 传到更低的网络层进行处理。

这里的一些细节注意点:

会克隆出一个新的skb :因为 skb 后续在调用网络层,最后到达网卡发送完成的时候,这个 skb 会被释放掉。而我们知道 TCP 协议是支持丢失重传的,在收到对方的 ACK 之前,这个 skb 不能被删除。所以内核的做法就是每次调用网卡发送的时候,实际上传递出去的是 skb 的一个拷贝。等收到 ACK 再真正删除。

skb 的结构:skb 内部其实包含了网络协议中所有的 header。在设置 TCP 头的时候,只是把指针指向 skb 的合适位置。后面再设置 IP 头的时候,在把指针挪一挪就行,避免频繁的内存申请和拷贝,效率很高。

然后将端口这类的数据进行写入,就发送给下一层的网络层处理

image.png

4.3 网络层发送处理

在网络层里主要处理路由项查找、IP 头设置、netfilter 过滤、skb 切分(大于 MTU 的话)等几项工作,处理完这些工作后会交给更下层的邻居子系统来处理。

image.png

如果数据大于 MTU 的话,是会执行分片的。

image.png

小知识点:

1,MTU(Maximum Transmission Unit,MTU),最大传输单元,一般为1500

2,Ip层出现分片,即使丢失了一片数据,也会重传全部的分片,因为IP层本身没有超时重传机制——由更高层(比如TCP)来负责超时和重传。当来自TCP报文段的某一片丢失后,TCP在超时后会重发整个TCP报文段,该报文段对应于一份IP数据报(而不是一个分片),没有办法只重传数据报中的一个数据分片。

  1. UDP容易导致IP分片,TCP不会导致,tcp到MSS的时候会自动分片,MSS为 1460 = 1500 – 40(IP报文头部长度)

4.4 邻居子系统

邻居子系统是位于网络层和数据链路层中间的一个系统,其作用是对网络层提供一个封装,让网络层不必关心下层的地址信息,让下层来决定发送到哪个 MAC 地址。

image.png

在邻居子系统里主要是查找或者创建邻居项,在创造邻居项的时候,有可能会发出实际的 arp 请求。然后封装一下 MAC 头,将发送过程再传递到更下层的网络设备子系统。大致流程如图。

image.png

4.5 网络设备子系统

image.png

网卡是有多个发送队列的(尤其是现在的网卡)。上面对 netdev_pick_tx 函数的调用就是选择一个队列进行发送。

4.6 软中断调度

image.png

4.7 igb 网卡驱动发送

image.png

在这里从网卡的发送队列的 RingBuffer 中取下来一个元素,并将 skb 挂到元素上。

4.8 发送完成硬中断

image.png

无非就是清理了 skb,解除了 DMA 映射等等

小结

image.png

3.发送网络数据的时候都涉及到哪些内存拷贝操作?

这里的内存拷贝,我们只特指待发送数据的内存拷贝。

第一次拷贝操作是内核申请完 skb 之后,这时候会将用户传递进来的 buffer 里的数据内容都拷贝到 skb 中。如果要发送的数据量比较大的话,这个拷贝操作开销还是不小的。

第二次拷贝操作是从传输层进入网络层的时候,每一个 skb 都会被克隆一个新的副本出来。网络层以及下面的驱动、软中断等组件在发送完成的时候会将这个副本删除。传输层保存着原始的 skb,在当网络对方没有 ack 的时候,还可以重新发送,以实现 TCP 中要求的可靠传输。

第三次拷贝不是必须的,只有当 IP 层发现 skb 大于 MTU 时才需要进行。会再申请额外的 skb,并将原来的 skb 拷贝为多个小的 skb。

参考

mp.weixin.qq.com/s/wThfD9th9…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享