一、相关实际问题查看内核发送数据消耗的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_sendmsg2)传输层处理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 =