一、相关实际问题
- 查看内核发送数据消耗的CPU时应该看sy还是si
- 在服务器上查看/proc/softirqs,为什么NET_RX要比NET_TX大得多
- 发送网络数据的时候都涉及那些内存拷贝操作
- 零拷贝到底是怎么回事
- 为什么Kafka的网络性能很突出
二、网络包发送过程总览
- 调用系统调用send发送
- 内存拷贝
- 协议处理
- 进入驱动RingBuffer
- 实际发送
- 中断通知发送完成
- 清理RingBuffer
三、网卡启动准备
现在的服务器上的网卡一般都是支持多队列的。每一个队列都是由一个RingBuffer表示的,开启了多队列以后的网卡就会有多个RingBuffer。
在多核时代,NIC 也相应的提供了 Multi-Queue 功能,可以将多个 Queue 通过硬中断绑定到不同的 CPU Cores 上处理。
以 Intel 82575 为例。
在硬件层面:它拥有 4 组硬件队列,它们的硬中断分别绑定到 4 个 Core 上,并通过 RSS(Receive Side Scaling)技术实现负载均衡。RSS 技术通过 HASH Packet Header IP 4-tuple(srcIP、srcPort、dstIP、dstPort),将同一条 Flow 总是送到相同的队列,从而避免了报文乱序问题。
在软件层面:Linux Kernel v2.6.21 开始支持网卡多队列特性。在 Net driver 初始化流程中,Kernel 获悉 Net device 所支持的硬件队列数量。然后结合 CPU Cores 的数量,通过 Sum=Min(NIC queue, CPU core) 公式计算得出应该被激活 Sum 个硬件队列,并申请 Sum 个中断号,分配给激活的每个队列。
网卡启动时最重要的任务就是分配和初始化RingBuffer,在网卡启动的时候会调用到__igb_open函数,RingBuffer就是在这里分配的。
// kernel/drivers/net/ethernet/intel/igb/igb_main.c static int __igb_open(struct net_device *netdev, bool resuming) { // 分配传输描述符数组 err = igb_setup_all_tx_resources(adpater); // 分配接收描述符数组 err = igb_setup_all_rx_resources(adpater); // 注册中断处理函数 err = igb_request_irq(adapter); if(err) goto err_req_irq; // 启用NAPI for(i = 0; i < adapter->num_q_vectors; i++) napi_enable(&(adapter->q_vector[i]->napi)); ...... } static int igb_setup_all_tx_resources(struct igb_adapter *adapter) { // 有几个队列就构造几个RingBuffer for(int i = 0; i < adapter->num_tx_queues; i++) { igb_setup_tx_resources(adapter->tx_ring[i]); } }igb_setup_tx_resources:
/** * igb_setup_tx_resources - allocate Tx resources (Descriptors) * @tx_ring: tx descriptor ring (for a specific queue) to setup * * Return 0 on success, negative on failure **/ int igb_setup_tx_resources(struct igb_ring *tx_ring) { struct device *dev = tx_ring->dev; int size; size = sizeof(struct igb_tx_buffer) * tx_ring->count; tx_ring->tx_buffer_info = vmalloc(size); //内核使用的数组 if (!tx_ring->tx_buffer_info) goto err; /* round up to nearest 4K */ 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, &tx_ring->dma, GFP_KERNEL);//硬件数组的DMA映射 if (!tx_ring->desc) goto err; tx_ring->next_to_use = 0; tx_ring->next_to_clean = 0; return 0; err: vfree(tx_ring->tx_buffer_info); tx_ring->tx_buffer_info = NULL; dev_err(dev, "Unable to allocate memory for the Tx descriptor ring\n"); return -ENOMEM; }igb_setup_tx_resources内部也是申请了两个数组,igb_tx_buffer数组和e1000_adv_tx_desc数组,一个供内核使用,一个供网卡硬件使用。
在这个时候它们之间还没什么关系,将来在发送数据的时候这两个数组的指针都指向同一个skb,这样内核和硬件就能共同访问同样的数据了。内核往skb写数据,网卡硬件负责发送。
硬中断的处理函数igb_msix_ring也是在__igb_open函数中注册的。
四、数据从用户进程到网卡的详细过程
1)系统调用实现
send系统调用内部真正使用的是sendto系统调用,主要做了两件事:
- 在内核中把真正的socket找出来
- 构造struct msghdr对象, 把用户传入的数据,比如buffer地址(用户待发送数据的指针)、数据长度、发送标志都装进去
SYS_CALL_DEFINE6(sendto, ......) { sock = sockfd_lookup_light(fd, &err, &fput_needed); struct msghdr msg; struct iovec iov; iov.iov_base = buff; iov.iov_len = len; msg.msg_iovlen = &iov; msg.msg_iov = &iov; msg.msg_flags = flags; ...... sock_sendmsg(sock, &msg, len); }sock_sendmsg经过一系列调用,最终来到__sock_sendmsg_nosec中调用sock->ops->sendmsg
对于AF_INET协议族的socket,sendmsg的实现统一为inet_sendmsg
2)传输层处理
1. 传输层拷贝
在进入协议栈inet_sendmsg以后,内核接着会找到sock中具体的协议处理函数,对于TCP协议而言,sk_prot操作函数集实例为tcp_prot,其中.sendmsg的实现为tcp_sendmsg(对于UDP而言中的为udp_sendmsg)。
int inet_sendmsg(......) { ...... return sk->sk_prot->sendmsg(iocb, sk, msg, size); } int tcp_sendmsg(......) { ...... // 获取用户传递过来的数据和标志 iov = msg->msg_iov; // 用户数据地址 iovlen = msg->msg_iovlen; // 数据块数为1 flags =