25 张图,一万字,拆解 Linux 网络包发送过程
时间:2021-08-19 16:30:43
手机看文章
扫描二维码
随时随地手机看文章
[导读]在开始今天的文章之前,我先来请大家思考几个小问题。问1:我们在查看内核发送数据消耗的CPU时,是应该看sy还是si?问2:为什么你服务器上的/proc/softirqs里NET_RX要比NET_TX大的多的多?问3:发送网络数据的时候都涉及到哪些内存拷贝操作?这些问题虽然在线上经...
在开始今天的文章之前,我先来请大家思考几个小问题。
- 问1:我们在查看内核发送数据消耗的 CPU 时,是应该看 sy 还是 si ?
- 问2:为什么你服务器上的 /proc/softirqs 里 NET_RX 要比 NET_TX 大的多的多?
- 问3:发送网络数据的时候都涉及到哪些内存拷贝操作?
int main(){
fd = socket(AF_INET, SOCK_STREAM, 0);
bind(fd, ...);
listen(fd, ...);
cfd = accept(fd, ...);
// 接收用户请求
read(cfd, ...);
// 用户请求处理
dosometing();
// 给用户返回结果
send(cfd, buf, sizeof(buf), 0);
}
今天我们来讨论上述代码中,调用 send 之后内核是怎么样把数据包发送出去的。本文基于Linux 3.10,网卡驱动采用Intel的igb网卡举例。预警:本文共有一万多字,25 张图,长文慎入!一、Linux 网络发送过程总览
我觉得看 Linux 源码最重要的是得有整体上的把握,而不是一开始就陷入各种细节。我这里先给大家准备了一个总的流程图,简单阐述下 send 发送了的数据是如何一步一步被发送到网卡的。在这幅图中,我们看到用户数据被拷贝到内核态,然后经过协议栈处理后进入到了 RingBuffer 中。随后网卡驱动真正将数据发送了出去。当发送完成的时候,是通过硬中断来通知 CPU,然后清理 RingBuffer。因为文章后面要进入源码,所以我们再从源码的角度给出一个流程图。虽然数据这时已经发送完毕,但是其实还有一件重要的事情没有做,那就是释放缓存队列等内存。
那内核是如何知道什么时候才能释放内存的呢,当然是等网络发送完毕之后。网卡在发送完毕的时候,会给 CPU 发送一个硬中断来通知 CPU。更完整的流程看图:注意,我们今天的主题虽然是发送数据,但是硬中断最终触发的软中断却是 NET_RX_SOFTIRQ,而并不是 NET_TX_SOFTIRQ !!!(T 是 transmit 的缩写,R 表示 receive)
意不意外,惊不惊喜???所以这就是开篇问题 1 的一部分的原因(注意,这只是一部分原因)。
问1:在服务器上查看 /proc/softirqs,为什么 NET_RX 要比 NET_TX 大的多的多?传输完成最终会触发 NET_RX,而不是 NET_TX。所以自然你观测 /proc/softirqs 也就能看到 NET_RX 更多了。好,现在你已经对内核是怎么发送网络包的有一个全局上的把握了。不要得意,我们需要了解的细节才是更有价值的地方,让我们继续!!
二、网卡启动准备
现在的服务器上的网卡一般都是支持多队列的。每一个队列上都是由一个 RingBuffer 表示的,开启了多队列以后的的网卡就会对应有多个 RingBuffer。网卡在启动时最重要的任务之一就是分配和初始化 RingBuffer,理解了 RingBuffer 将会非常有助于后面我们掌握发送。因为今天的主题是发送,所以就以传输队列为例,我们来看下网卡启动时分配 RingBuffer 的实际过程。在网卡启动的时候,会调用到 __igb_open 函数,RingBuffer 就是在这里分配的。
//file: drivers/net/ethernet/intel/igb/igb_main.c
static int __igb_open(struct net_device *netdev, bool resuming)
{
struct igb_adapter *adapter = netdev_priv(netdev);
//分配传输描述符数组
err = igb_setup_all_tx_resources(adapter);
//分配接收描述符数组
err = igb_setup_all_rx_resources(adapter);
//开启全部队列
netif_tx_start_all_queues(netdev);
}
在上面 __igb_open 函数调用 igb_setup_all_tx_resources 分配所有的传输 RingBuffer, 调用 igb_setup_all_rx_resources 创建所有的接收 RingBuffer。//file: drivers/net/ethernet/intel/igb/igb_main.c
static int igb_setup_all_tx_resources(struct igb_adapter *adapter)
{
//有几个队列就构造几个 RingBuffer
for (i = 0; i < adapter->num_tx_queues; i ) {
igb_setup_tx_resources(adapter->tx_ring[i]);
}
}
真正的 RingBuffer 构造过程是在 igb_setup_tx_resources 中完成的。//file: drivers/net/ethernet/intel/igb/igb_main.c
int igb_setup_tx_resources(struct igb_ring *tx_ring)
{
//1.申请 igb_tx_buffer 数组内存
size = sizeof(struct igb_tx_buffer) * tx_ring->count;
tx_ring->tx_buffer_info = vzalloc(size);
//2.申请 e1000_adv_tx_desc DMA 数组内存
tx_ring->size = tx_ring->count * sizeof(union e1000_adv_tx_desc);
tx_ring->size = ALIGN(tx_ring->size, 4096);
tx_ring->desc = dma_alloc_coherent(dev, tx_ring->size,