当前位置: 首页 > news >正文

深入理解Linux内核网络(三):内核发送网络包

本文章详细介绍了 Linux 系统中网络包从用户进程到网卡的完整发送流程。内容涵盖网卡启动准备、系统调用的实现、传输层和网络层的处理、邻居子系统的 MAC 地址解析、网络设备子系统的排队与调度、软中断调度机制,以及最终由 IGB 网卡驱动完成的硬件数据发送。

部分内容来源于 《深入理解Linux网络》、《Linux内核源码分析TCP实现》

网络包发送过程总览

在服务端程序中,当调用 send(cfd, buf, sizeof(buf), 0) 时,数据包的发送过程实际上涉及多个步骤。

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

源码角度

在内核中,send() 会触发 SYSCALL_DEFINE6(sendto) 这个系统调用。

SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len, ...) {// 构建消息头结构体 msghdrstruct msghdr msg;// 处理用户数据并发送return sock_sendmsg(sock, &msg, len);
}

这里,sendto() 构建了消息头 msghdr,并调用 sock_sendmsg() 函数,其是在内核 socket 层处理数据发送的主要函数,它内部进一步调用协议相关的发送函数:

static inline int __sock_sendmsg_nosec(...) {// 调用协议栈对应的发送函数return sock->ops->sendmsg(iocb, sock, msg, size);
}

其中 sock->ops->sendmsg() 是根据具体的协议进行进一步的处理(如 TCP 或 UDP)。在 TCP 协议中,inet_sendmsg() 会调用 tcp_sendmsg() 来处理传输:

int tcp_sendmsg(...) {// 处理 TCP 发送逻辑...return tcp_transmit_skb(skb);
}

tcp_sendmsg() 负责构建 TCP 包,并调用 tcp_transmit_skb() 函数来将报文传递到传输层。

static int tcp_transmit_skb(...) {struct tcphdr *th = tcp_hdr(skb);  // 获取 TCP 头部th->source = inet->inet_sport;     // 设置源端口th->dest = inet->inet_dport;       // 设置目的端口// 调用下一层处理函数return icsk_af_ops->queue_xmit(skb, ...);
}

tcp_transmit_skb() 组装 TCP 头部信息并将数据交给网络层,ip_queue_xmit() 函数负责发送 IP 数据包:

int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl) {// 处理本地输出res = ip_local_out(skb);return res;
}

接着,ip_local_out() ip_finish_output2() 负责进一步将数据包传递给下一层(邻居子系统):

static inline int ip_finish_output2(...) {return dst_neigh_output(dst, neigh, skb);
}

邻居子系统通过 dst_neigh_output() 调用硬件接口,而neigh_hh_output() 进一步调用设备队列接口

static inline int dst_neigh_output(...) {return neigh_hh_output(hh, skb);
}
static inline int neigh_hh_output(...) {skb_push(skb, hh->hh_len);return dev_queue_xmit(skb);  // 发送到网络设备队列
}

网络设备子系统通过dev_queue_xmit()将数据包发送到硬件驱动,dev_hard_start_xmit() 是实际发送数据的函数:

int dev_queue_xmit(struct sk_buff *skb) {// 选择发送队列txq = netdev_pick_tx(dev, skb);return __dev_xmit_skb(skb, dev, txq);
}
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev) {const struct net_device_ops *ops = dev->netdev_ops;return ops->ndo_start_xmit(skb, dev);
}

在驱动层,具体的设备驱动(如英特尔 igb 网卡驱动)负责与硬件进行交互:

static netdev_tx_t igb_xmit_frame(struct sk_buff *skb, struct igb_ring *tx_ring) {// 从 TX 队列获取缓冲区并发送return igb_tx_map(tx_ring, first, hdr_len);
}

最后,驱动程序与网卡硬件交互,通过硬件发送数据。

小结

首先应用层发出 send() 调用,通过系统调用接口进入内核。系统调用层负责处理用户空间的请求,构建消息数据,并将其交给内核的网络协议栈。

协议层根据使用的协议(例如 TCP 或 UDP)进行相应的处理。对于 TCP 协议,内核会构建 TCP 数据包,添加必要的头部信息,并将数据发送到传输层。传输层则负责将数据封装成 IP 数据包,准备好供网络传输。

数据包经过网络层后,发送到邻居子系统,负责解析邻居的硬件地址信息,并将数据传递给网络设备子系统。网络设备子系统处理具体的网络接口,选择适当的发送队列,并通过调用驱动程序将数据交给物理网卡设备。

在驱动层,设备驱动程序根据网卡硬件的要求准备数据,并最终将其发送到硬件,完成整个数据发送过程。这个过程从高层的应用调用开始,逐层处理数据的封装与传输,最终通过硬件将数据发送到网络。

网卡启动准备

多队列支持及分配和初始化

现代网卡支持多队列,每个队列都有一个独立的环形缓冲区(RingBuffer)。网卡启动时的关键任务是为每个队列分配 RingBuffer 并进行初始化。当网卡启动时,会调用 __igb_open 函数,该函数负责初始化网卡的各项资源,包括为每个传输队列分配和初始化 RingBuffer。

static int __igb_open(struct net_device *netdev, bool resuming)
{struct igb_adapter *adapter = netdev_priv(netdev);int err;// 分配传输描述符数组(为每个传输队列分配 RingBuffer)err = igb_setup_all_tx_resources(adapter);if (err)return err;// 分配接收描述符数组(为每个接收队列分配 RingBuffer)err = igb_setup_all_rx_resources(adapter);if (err)goto err_setup_rx;// 注册中断处理函数err = igb_request_irq(adapter);if (err)goto err_req_irq;// 启用 NAPI 机制,处理网络数据包接收for (int i = 0; i < adapter->num_q_vectors; i++) {napi_enable(&(adapter->q_vector[i]->napi));}// 网卡启动后的其他操作igb_configure(adapter);igb_up(adapter);return 0;err_req_irq:igb_free_all_rx_resources(adapter);
err_setup_rx:igb_free_all_tx_resources(adapter);return err;
}

具体分配过程

igb_setup_all_tx_resources 函数中,网卡将为每个传输队列(tx_ring)分配资源,在这个函数内部,每个传输队列的 RingBuffer 通过 igb_setup_tx_resources 函数来设置。

static int igb_setup_all_tx_resources(struct igb_adapter *adapter)
{for (int i = 0; i < adapter->num_tx_queues; i++) {int err = igb_setup_tx_resources(adapter->tx_ring[i]);if (err)return err;}return 0;
}
static int igb_setup_tx_resources(struct igb_ring *tx_ring)
{// 分配传输缓冲区(供内核使用)tx_ring->tx_buffer = vzalloc(sizeof(struct igb_tx_buffer) * tx_ring->count);if (!tx_ring->tx_buffer)return -ENOMEM;// 分配硬件描述符(供网卡硬件使用)tx_ring->desc = dma_alloc_coherent(tx_ring->dev, tx_ring->size, &tx_ring->dma, GFP_KERNEL);if (!tx_ring->desc)return -ENOMEM;return 0;
}

注册中断处理函数

igb_msix_ring是中断处理程序,负责在网卡硬件触发中断时,处理相关任务。这是在 igb_request_irq 中注册的:

static int igb_request_irq(struct igb_adapter *adapter)
{int err;for (int i = 0; i < adapter->num_q_vectors; i++) {struct igb_q_vector *q_vector = adapter->q_vector[i];err = request_irq(q_vector->irq, igb_msix_ring, 0, q_vector->name, q_vector);if (err)return err;}return 0;
}

NAPI 启动

NAPI(New API)是 Linux 内核中的一种机制,用于减少网络中断次数,提高高负载下的网络性能。在网卡启动时为每个队列启用 NAPI:

for (int i = 0; i < adapter->num_q_vectors; i++) {napi_enable(&(adapter->q_vector[i]->napi));
}

发送数据时的协同工作

在发送数据的过程中,igb_tx_buffer(供内核使用)和 e1000_adv_tx_desc(供网卡硬件使用)的指针会指向同一个 skb,这样内核和硬件都能访问同一个数据包。

struct igb_tx_buffer *buffer = &tx_ring->tx_buffer[next_to_use];
struct e1000_adv_tx_desc *tx_desc = E1000_TX_DESC_ADV(tx_ring, next_to_use);// 内核往 buffer 写入数据
buffer->skb = skb;// 硬件描述符指向同样的 skb 数据
tx_desc->buffer_addr = cpu_to_le64(dma_map_single(tx_ring->dev, skb->data, skb_headlen(skb), DMA_TO_DEVICE));

这样,内核负责向 skb 写入数据,而网卡硬件则通过描述符读取这些数据,并负责将其发送出去。

小结

多队列网卡

支持多队列的网卡可以为不同的 CPU 核心分配各自的队列,减少队列之间的锁争用,提高网络性能。每个队列使用一个独立的 RingBuffer 来存储和管理待发送或接收的数据包。

网卡启动时,内核通过调用 __igb_open 函数进行初始化。在这个过程中,内核需要为每个传输和接收队列分配相应的资源,并进行一系列设置:

分配和初始化 RingBuffer

在网卡启动时,内核需要为每个队列分配 RingBuffer。传输和接收队列的 RingBuffer 结构不同,但它们都用于缓存网络数据包。每个队列的 RingBuffer 负责:

  • 传输队列(TX RingBuffer):存储待发送的数据包。
  • 接收队列(RX RingBuffer):存储从网络接收的数据包。

RingBuffer 的分配过程主要包括为内核和网卡硬件分别分配缓冲区。这些缓冲区在后续的数据传输过程中会共享相同的数据指针,以便内核和硬件能够协同工作。内核负责将数据写入缓冲区,而网卡硬件负责从缓冲区中读取数据并发送到网络上。

资源分配

在分配传输和接收资源时,网卡为每个队列分配了两个关键的数组:

  • 内核缓冲区数组:用于存储内核需要传输的数据包(如 skb,Socket Buffer)。
  • 硬件描述符数组:这是网卡硬件使用的缓冲区。当网卡启动发送操作时,硬件会读取这些描述符来获取要发送的数据。

这两个数组在网卡发送数据时会指向同一个数据包(即 skb)

中断处理的设置

在网卡启动时,还需要为每个队列注册中断处理程序。当网络设备接收到数据或完成数据发送时,网卡会通过硬件中断通知 CPU。内核注册的中断处理程序会负责响应这些中断并处理相关的任务,如:

  • 数据包的接收:当网卡接收到数据时,触发中断,通知内核读取接收到的数据。
  • 发送完成的通知:当网卡完成某个数据包的发送时,通过中断通知内核,以便进行后续的操作。

中断处理程序是在网卡启动时通过 igb_request_irq 函数注册的,负责在接收到硬件中断时调用相应的处理逻辑。

NAPI 机制的启用

NAPI 通过将数据包处理延迟到轮询机制,而不是每次网络中断时立即处理数据包,从而降低系统负担。

发送数据时的协同工作

在数据发送过程中,内核和网卡硬件通过共享同一个数据包(skb)来实现协同工作。

  • 内核负责写入数据:当应用程序发送数据时,内核将数据写入 skb,并通过内核缓冲区管理这些数据包。
  • 网卡硬件负责发送数据:网卡硬件通过读取硬件描述符数组中的信息,获取要发送的数据包地址,并将数据包发送到网络上。

数据从用户进程到网卡的详细过程

send系统调用

send() 函数调用会进入内核空间,调用的是 sendto() 系统调用。sendto() send() 的底层实现,负责处理网络传输。它主要完成以下两项任务:

  • 找到 socket:根据文件描述符查找到内核中的 socket 对象,这个对象在内核中由 struct sock 表示,代表一个特定的网络连接。
  • 构造消息头(msghdr):构建包含发送数据的信息结构体,之后将该信息传递给下层的协议栈。

在这里插入图片描述

sendto() 完成 socket 查找和消息头构造之后,接下来会调用 sock_sendmsg() 函数。这个函数位于内核的 socket 层,主要负责将数据包传递给协议栈。它执行的步骤如下:

  • 使用已经找到的 socket,确定该 socket 对应的协议(如 TCP 或 UDP)。
  • 调用 socket 操作集中的 sendmsg() 方法。

在这个过程中,sock_sendmsg() 不直接处理协议细节,而是通过协议操作集,将数据传递给具体的协议栈函数。

每个 socket 对象对应一个特定的协议(例如 TCP 或 UDP)。在 struct sock 结构体中,包含了两个重要的字段:

  • sk_prot:该字段指向该 socket 使用的协议,比如 TCP 套接字的协议指针会指向 TCP 协议操作函数。
  • ops:这是 struct proto_ops 结构体,定义了该协议的操作函数集合,例如 sendmsg()recvmsg() 等。它们指向该协议对应的操作函数集,具体负责处理发送、接收等操作。

sock_sendmsg() 被调用时,它会调用 ops 中的 sendmsg() 函数,这个函数最终会由协议栈中的具体实现来处理,比如 TCP 的 inet_sendmsg()

传输层

inet_sendmsg()中,内核找到该 socket 关联的具体协议(此处是 TCP),并调用 TCP 协议的 tcp_sendmsg() 函数。

在这里插入图片描述

tcp_sendmsg 是 TCP 协议处理发送数据的核心函数,它完成了从用户数据到 TCP 数据包的封装。这个函数负责将数据从用户空间拷贝到内核中的 skb(socket buffer),并将 skb 加入到 TCP 的发送队列中。以下是关键步骤:

检查 TCP 连接状态: tcp_sendmsg 检查当前 TCP 连接的状态。如果连接未建立(比如尚未完成三次握手),进程可能会进入睡眠等待,直到连接进入 ESTABLISHED 状态。在某些情况下(如使用 TCP Fast Open),可以在发送 SYN 包时携带数据,但这是特例。

获取 MSS 和 size_goal: 函数接着获取当前 TCP 连接的 MSS(最大报文段长度)以及 size_goal(理想的数据包大小)。size_goal 受多种因素影响,包括 MSS、网络的 MTU(最大传输单元)、TCP 的窗口大小等。如果网卡支持 GSO(大分段卸载),size_goal 可以是 MSS 的整数倍。

遍历用户数据块tcp_sendmsg 函数会遍历用户空间的数据块,将数据逐块拷贝到内核中。检查发送队列最后一个 skb 是否有空间可以继续追加数据。如果 skb 未满,数据将被追加到该 skb 中。如果最后一个 skb 已经满了或没有未发送的数据,则申请一个新的 skb。

数据拷贝到 skb: 当需要新的 skb 时,函数会申请一个,并将其添加到 socket 的发送队列中。数据从用户空间拷贝到 skb 的两个主要存储区域:

  • 线性数据区:如果 skb 的线性数据区有足够空间,数据将被复制到该区域,同时计算校验和。
  • 分页区:如果线性数据区不够,tcp_sendmsg 会使用分页区存储数据。分页区用于处理大数据包,避免线性区的内存分配不足。

发送判断: 数据拷贝完成后,tcp_sendmsg 会判断是否立即发送数据。这取决于多种因素,包括 TCP 的发送窗口、网络拥塞状况等。TCP 协议通过滑动窗口和拥塞控制算法来决定是否允许立即发送数据,或者需要等待确认。

在这里插入图片描述

一旦判断满足发送条件,函数会调用tcp_write_xmit 函数执行实际的发送操作。tcp_write_xmit 负责将 skb 中的数据发送到网络中,并执行 TCP 的滑动窗口和拥塞控制操作。而tcp_transmit_skb 是传输层发送数据的最后一步。该函数会根据 TCP 协议的需求,设置 TCP 头部信息,并调用网络层的发送接口将数据包发送出去,其操作如下:

  • 克隆 skb:为了保证 TCP 的可靠传输,内核会克隆一个新的 skb。因为 TCP 支持重传,未收到 ACK 确认的数据不能被删除,所以克隆后的 skb 会被用于发送,而原 skb 保留用于重传。
  • 设置 TCP 头部:函数根据当前连接的状态,设置 TCP 包的头部信息,例如源端口、目的端口、窗口大小等。
  • 调用网络层发送接口:调用网络层的发送函数,将封装好的数据包传递给网络层,进行进一步处理。

网络层

在 TCP 协议下,传输层调用 tcp_queue_xmit 函数将数据包传递给网络层。网络层的处理流程包括路由查找、IP 头的设置、Netfilter 过滤、分片以及将数据传递给邻居子系统。

在这里插入图片描述
ip_queue_xmit: 查找路由并设置 IP 头。函数检查 socket 是否已经缓存了相关的路由表项。如果缓存了路由表,直接使用缓存的路由信息来构造数据包。如果没有缓存,则查找适合的路由表项,并将其缓存到 socket 中。之后为数据包设置 IP 头部信息。IP 头包括协议号、TTL(存活时间)、源地址和目的地址等。这个步骤确保数据包能够在网络中正确路由和传输。

ip_local_out:Netfilter 过滤。主要负责调用 __ip_local_out,并通过 nf_hook 进行 Netfilter 过滤。如果系统配置了 iptables 或其他防火墙规则,Netfilter 会在这一阶段执行数据包过滤、修改或 NAT(网络地址转换)等操作。

ip_output:统计与再次 Netfilter 过滤。此步骤统计了传输的数据包信息,确保网络层能够准确监控数据包的传输。再次进行 Netfilter 过滤。确保数据包在离开网络层之前,仍然符合防火墙规则或其他策略要求。

ip_finish_output:分片处理 。这个函数主要负责处理 MTU(最大传输单元)相关的问题。如果数据包的长度超过了 MTU会将数据包进行分片。分片的代价是性能开销增加,并且如果任意一个分片丢失,整个数据包都需要重传。如果数据包不需要分片,直接调用 ip_finish_output2,将数据包传递给下一步的处理层。

dst_neigh_output:邻居子系统 。在 ip_finish_output2 中,根据数据包的目的 IP 地址,内核查找下一跳的邻居项。如果找到了邻居项,则将数据包传递给邻居子系统。如果没有找到邻居项,内核会创建新的邻居表项。

邻居子系统

邻居子系统的主要职责是处理下一跳的邻居项(如 ARP 请求和 MAC 地址解析),并将网络层的数据包封装成以太网帧(包含 MAC 头)。

在这里插入图片描述查找或创建邻居项

  • rt_nexthop:首先,函数从路由表中获取下一跳的 IP 地址,称为 rt_nexthop。
  • __ipv4_neigh_lookup_noref:根据下一跳 IP 地址,在 ARP 缓存中查找对应的邻居项。如果找到了,则直接使用这个邻居项。
  • __neigh_create:如果邻居项未找到,系统会调用 __neigh_create 创建一个新的邻居项,并将其加入邻居表(哈希表中)。创建邻居项的过程中,可能会发出 ARP 请求来解析下一跳的 MAC 地址。
static inline struct neighbour *ipv4_neigh_lookup_noref(struct net_device *dev, u32 key) {// 从 arp_tbl 中获取邻居哈希表struct neigh_hash_table *nht = rcu_dereference_bh(arp_tbl.nht);// 计算哈希值,用于快速查找hash_val = arp_hashfn(dev, key);// 遍历哈希桶中的邻居项链表for (n = rcu_dereference_bh(nht->hash_buckets[hash_val]); n != NULL; n = rcu_dereference_bh(n->next)) {// 检查邻居项是否与目标 IP 匹配if (n->dev == dev && *(u32 *)n->primary_key == key)return n;  // 找到邻居项}return NULL;  // 未找到邻居项
}
struct neighbour *_neigh_create(struct neigh_table *tbl, struct net_device *dev, const void *pkey) {struct neighbour *n;// 分配一个新的邻居项n = neigh_alloc(tbl, dev);memcpy(n->primary_key, pkey, tbl->key_len);n->dev = dev;// 调用 neigh_setup 进行邻居项的设置n->parms->neigh_setup(n);// 将新创建的邻居项添加到邻居哈希表中rcu_assign_pointer(tbl->nht->hash_buckets[hash_val], n);return n;
}

封装 MAC 头和发送

在找到或创建邻居项后,系统将为数据包添加 MAC 头(包含源 MAC 地址和目的 MAC 地址)。如果需要 ARP 请求来解析 MAC 地址,则系统会等待 ARP 应答。封装完成后,数据包会被传递给 dev_queue_xmit 函数,该函数将数据包传递给网络设备子系统进行进一步处理。

网络设备子系统

dev_queue_xmit 函数是网络设备子系统处理的入口。网络设备子系统负责将邻居子系统传递的以太网帧通过网卡发送到物理网络。

在这里插入图片描述
选择发送队列: dev_queue_xmit 会首先根据 XPS(传输处理器选择)或通过 skb_tx_hash 函数计算出应该使用的发送队列。每个发送队列对应网卡的一个硬件环形缓冲区(Ring Buffer),网卡会从这些缓冲区中取数据进行发送。

获取排队规则(qdisc): Linux 使用 qdisc(队列规则)来管理数据包的排队和发送。每个发送队列都有一个关联的 qdisc,用于控制数据包的发送顺序、调度策略和流量控制等。常见的 qdisc 类型有 pfifo_fast(FIFO)mq(多队列)、tbf(令牌桶过滤器)和 htb(分层令牌桶)。之后调用 __dev_xmit_skb 函数继续处理数据包的发送。

排队和发送: 根据 qdisc 的配置,数据包可能直接发送,或者根据优先级、排队规则先进行排队。dev_queue_xmit 可以绕过排队系统直接发送数据(如果系统配置允许),也可能需要排队等待。

从队列中发送数据包: __qdisc_run 函数从队列中取出数据包并发送。它会循环调用 qdisc_restart,从 qdisc 队列中获取 skb,然后进行发送。如果系统负载较高或者配额(quota)用尽,__qdisc_run 会暂停发送并触发软中断处理(NET_TX_SOFTIRQ)进行后续的发送任务。

数据包发送逻辑:qdisc_restart 中,首先调用 dequeue_skb 从队列中取出要发送的 skb(数据包)。如果队列中没有数据包,则返回 0,退出循环。如果获取到了数据包,则调用 sch_direct_xmit (直接调用网络驱动)发送数据。

软中断调度

当 CPU 资源耗尽时,系统会调用 __netif_schedule 函数,将网络设备的发送队列放入软中断的数据结构 softnet_data 中,并设置软中断。接下来的发送操作将由软中断处理,而不再消耗用户进程的系统时间。

在这里插入图片描述
NET_TX_SOFTIRQ 软中断被触发时,系统会进入 net_tx_action 函数,这个函数是网络层中用于处理发送队列的软中断处理程序。

static void net_tx_action(struct softirq_action *h) {struct softnet_data *sd = &__get_cpu_var(softnet_data);// 检查是否有待处理的输出队列if (sd->output_queue) {// 获取第一个qdisc队列struct Qdisc *head = sd->output_queue;// 遍历并处理所有发送队列while (head) {struct Qdisc *q = head;head = head->next_sched;// 调用qdisc_run处理队列中的数据包qdisc_run(q);}}
}
static inline void qdisc_run(struct Qdisc *q) {if (qdisc_run_begin(q)) {__qdisc_run(q);}
}

softnet_data 存储了当前 CPU 上要处理的发送队列信息。软中断从这里获取要处理的输出队列。在 qdisc_run 中,系统调用 __qdisc_run 来继续处理发送队列中的数据包。

无论是在普通队列处理(qdisc_restart)中,还是在软中断处理的上下文(net_tx_action -> __qdisc_run)中,最终数据包的发送都会调用 dev_hard_start_xmit 函数,该函数是网络驱动程序的发送接口,用于实际将数据包发送到网卡。

igb网卡驱动发送

igb 网卡驱动的发送流程是网络栈最终将数据发送到物理网络的关键环节。在前面已经介绍了内核网络栈是如何逐层处理数据并将其交给网络设备子系统,最终进入到网卡驱动的发送队列。接下来详细解读 igb 网卡驱动的发送逻辑.

在这里插入图片描述
dev_hard_start_xmit 是网卡驱动的通用接口,它在网络设备子系统调用时负责触发网卡驱动的实际发送函数。在 igb 网卡驱动中,ndo_start_xmit 函数指向的是 igb_xmit_frame,这是 igb 网卡驱动的发送函数。

int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev, struct netdev_queue *txq)
{const struct net_device_ops *ops = dev->netdev_ops;// 获取设备支持的功能列表features = netif_skb_features(skb);// 调用网卡驱动的发送回调函数 (igb_xmit_frame)skb_len = skb->len;rc = ops->ndo_start_xmit(skb, dev);
}

igb_xmit_frame 是 igb 网卡驱动的核心发送函数,它负责将网络数据包(skb)放入网卡的发送队列(RingBuffer)中,准备通过网卡硬件进行发送。igb_xmit_frame 通过调用 igb_xmit_frame_ring 函数将数据包加入特定的发送队列。

igb_xmit_frame_ring 函数将数据包(skb)挂载到网卡的发送队列(RingBuffer)中。网卡的发送队列是一组预先分配好的结构,称为 tx_buffer_info,它们存储着每个要发送的数据包的信息。

netdev_tx_t igb_xmit_frame_ring(struct sk_buff *skb, struct igb_ring *tx_ring)
{// 获取TX队列中下一个可用的缓冲区struct igb_tx_buffer *first = &tx_ring->tx_buffer_info[tx_ring->next_to_use];// 将 skb 数据包挂载到 tx_buffer 中first->skb = skb;first->bytecount = skb->len;first->gso_segs = 1;// 准备发送数据igb_tx_map(tx_ring, first, hdr_len);return NETDEV_TX_OK;
}

igb_tx_map 函数负责将 skb 数据映射到网卡可访问的 DMA 内存空间。DMA(直接内存访问)允许网卡直接从主机内存中读取数据,而无需 CPU 参与,这大大提高了数据传输的效率。

static void igb_tx_map(struct igb_ring *tx_ring, struct igb_tx_buffer *first, const u8 hdr_len)
{// 获取下一个可用的描述符指针tx_desc = IGB_TX_DESC(tx_ring, i);// 为skb->data构造内存映射,以允许设备通过DMA从RAM中读取数据dma = dma_map_single(tx_ring->dev, skb->data, size, DMA_TO_DEVICE);// 遍历该数据包的所有分片,为skb的每个分片生成有效映射for(frag = &skb_shinfo(skb)->frags[0]; ; flag++){tx_desc->read.buffer_addr = cpu_to_le64(dma);tx_desc->read.cmd_type_len = ...;tx_desc->read.olinfo_status = 0;}// 设置最后一个descriptorcmd_type |= size | IGB_TXD_DCMD;tx_desc->read.cmd_type_len = cpu_to_le32(cmd_type);
}

当所有描述符都正确填充好后,网卡硬件就可以通过 DMA 读取内存中的数据包并将其发送到网络。最终igb 驱动会通知硬件开始发送。

小结

用户态 send() 系统调用

用户进程通过 send() 函数发起数据发送请求,数据进入内核空间并交给套接字管理。内核首先通过套接字接口将数据传递给网络协议栈。此时,数据被封装为适合网络传输的格式。

网络协议栈处理

数据进入传输层(例如,TCP 或 UDP),根据所使用的协议进行相应的封装:

  • 封装传输层头部:如果是 TCP,数据被封装为 TCP 报文,并添加源端口、目的端口、序列号等信息。
  • 流量控制与窗口管理:TCP 使用流量控制和窗口机制,决定数据是否可以立即发送或需要排队。
  • 进入网络层:封装好的传输层数据报(如 TCP 数据包)被传递到网络层。

网络层处理

数据包被封装成 IP 数据报并准备进行路由。

  • 路由查找:系统查找目标 IP 地址的路由信息,确保数据包能够找到通向目的地的路径。如果缓存中没有路由信息,系统会进行动态路由查找。
  • IP 头部封装:为数据包添加 IP 头,包含源 IP 地址、目的 IP 地址、TTL 等信息。
  • 分片处理:如果数据包的大小超过了网络 MTU(最大传输单元),会进行分片处理,以确保数据可以通过网络传输。
  • Netfilter 过滤:数据包经过防火墙或 iptables 规则的检查,如果符合某些过滤规则,可能会被修改、丢弃或重新路由。

邻居子系统处理

网络层准备好数据包后,邻居子系统负责处理下一跳的 MAC 地址解析(ARP 请求):

  • 查找或创建邻居项:系统通过邻居表查找目标 IP 地址对应的 MAC 地址。如果没有找到匹配的邻居项,系统会发起 ARP 请求以解析目标 MAC 地址。
  • 封装 MAC 头:一旦解析到 MAC 地址,系统为数据包添加以太网帧头,包含源 MAC 地址和目标 MAC 地址。

网络设备子系统处理

在邻居子系统完成处理后,数据包进入网络设备子系统,并最终传递给网卡驱动:

  • 发送队列选择:对于多队列网卡,系统根据特定算法选择适当的队列(通常根据哈希算法或用户配置),每个发送队列对应一个硬件缓冲区。
  • 队列管理和调度:数据包根据 qdisc 队列规则管理系统(例如 FIFO 或流量控制规则)进行排队和调度,决定数据包的发送顺序和优先级。
  • 软中断处理:如果系统资源耗尽,发送过程会转移到软中断上下文,由软中断继续处理剩余的发送操作,减少用户进程的系统时间占用。

IGB 网卡驱动处理

数据包通过设备子系统传递到 IGB 网卡驱动,驱动负责管理网卡硬件发送:

  • 挂载到发送队列:数据包被挂载到网卡的 RingBuffer 中,RingBuffer 是网卡硬件的发送缓冲区,存储着即将发送的所有数据包。
  • DMA 映射:数据包被映射到设备的 DMA 地址空间,允许网卡硬件直接从内存中读取数据,而无需 CPU 参与,极大地提高了数据传输效率。
  • 触发网卡发送:网卡硬件读取缓冲区中的数据,通过网络接口将数据包实际发送到网络中。数据最终到达目标主机。

问题解答

查看 CPU 消耗时应该关注 sy(系统态)还是 si(软中断)?

在网络包的发送过程中,绝大部分的 CPU 消耗发生在用户进程的内核态(sy,即系统态时间),即用户进程调用内核进行数据包处理和发送。大部分情况下,网络数据的发送操作并不会触发软中断(si),因为这些操作直接由用户进程在内核态完成,包括协议栈处理、队列操作以及驱动程序的调用。

软中断(si) 只有在特定情况下(例如,当系统资源不足时)才会被触发,这时软中断机制(如 ksoftirqd 线程)接手剩下的发送工作。也就是说,系统在发送网络数据时,sy 的占比通常会比 si 高。

因此,当监控网络 I/O 对 CPU 造成的开销时,不能只看 si,还需要考虑 sy,因为用户进程在内核态完成了大部分工作,只有一小部分才由软中断处理。

为什么 /proc/softirqs 中的 NET_RX 比 NET_TX 大得多?

这是因为网络包的接收和发送在内核中的处理方式不同:

接收(NET_RX):每个接收到的数据包都需要经过软中断(NET_RX_SOFTIRQ),这个中断用于处理接收的数据包,将其传递给内核网络栈。接收过程中大量使用软中断来处理数据,因此 NET_RX_SOFTIRQ 的计数通常会很高。

发送(NET_TX):绝大多数的发送操作是在用户进程的内核态中完成的(sy 时间),并不会触发软中断。只有当系统资源不足时,发送操作才会被推迟并由 NET_TX_SOFTIRQ 处理。因此,发送操作对软中断的依赖性低,NET_TX_SOFTIRQ 的计数相对较少。

发送网络数据时涉及的内存拷贝操作

发送网络数据时,涉及多个内存拷贝操作,这些操作会对性能产生一定的影响,尤其在处理大数据时:

第一次拷贝:内核申请 skb 之后,会将用户传入的 buffer 数据拷贝到 skb 的内存中。这是标准的内核态和用户态之间的数据拷贝,尤其是大数据时,这个拷贝的开销会很大。

第二次拷贝(浅拷贝):当 TCP 传输层将数据包传递给网络层时,会克隆出 skb 的副本,以保留原始数据。这个副本主要用于在丢包时可以重传原始数据(TCP 的可靠传输机制),这里的拷贝通常是浅拷贝,即只拷贝 skb 的描述符,数据部分仍然复用。

第三次拷贝(分片时):如果 IP 层发现数据包大小超过了 MTU(最大传输单元),需要对数据包进行分片。此时,会进行额外的内存拷贝,将原始的 skb 分成多个较小的 skb。

零拷贝(Zero-Copy)技术

零拷贝 是一种减少数据拷贝次数的技术,旨在提高性能,特别是在发送大数据时显著减少 CPU 和内存开销。零拷贝的核心思想是:减少用户态与内核态之间的数据拷贝。

典型的网络发送流程中,涉及两次主要拷贝:

读数据:从硬盘读取文件时,数据先从硬盘拷贝到内核态的 Page Cache,再从 Page Cache 拷贝到用户空间的缓冲区。

发送数据:当用户调用 send() 时,数据会再次从用户空间拷贝到内核态的 socket 缓冲区,之后通过 DMA 传输到网卡。

零拷贝(例如通过 sendfile() 系统调用) 通过避免用户态和内核态的拷贝,直接从内核态的 Page Cache 将数据发送到网络接口,实现了性能的提升。这个过程减少了 CPU 的数据拷贝开销。

Kafka 的高性能网络设计

Kafka 的网络性能优异的一个重要原因是它采用了sendfile 系统调用来发送数据包。sendfile 是 Linux 内核提供的零拷贝机制之一,它直接从文件的 Page Cache 读取数据并发送到网络,而不经过用户态,避免了多次数据拷贝,提高了数据发送效率。


http://www.mrgr.cn/news/56223.html

相关文章:

  • 【其他】无法启动phptudy服务,提示错误2:系统找不到指定的文件
  • AFFINEQUANT: AFFINE TRANSFORMATION QUANTI ZATION FOR LARGE LANGUAGE MODELS阅读
  • LinkedList和链表之刷题课(上)
  • Prompt提示词设计:如何让你的AI对话更智能?
  • 提取图片内容的 Python 程序
  • Planetoid(helpers.dataset_classes文件中的classic_datasets.py)
  • 服务器的介绍
  • 服务器安装Anaconda,Anaconda安装Pytorch
  • 【信息安全服务】常见服务高危端口排查(含内网)
  • [Linux进程概念]命令行参数|环境变量
  • netty的网络IO模型
  • 音乐之趣:叶珂吐槽伍佰,黄晓明笑对人生
  • Git 完整教程:版本管理、分支操作与远程仓库解析
  • 链动 2+1 模式、AI 智能名片与 S2B2C 商城小程序:提升企业产品方便性的创新策略
  • 云原生技术:nacos进化到servicemash
  • 多模态数据融合最新Nature来袭!四种方法就上岸,可别错过了这波好思路!
  • RunMe_About BIOS Connect Server Auto Setting.cmd
  • 端口频繁遭遇攻击,又该如何应对?
  • 骨传导耳机哪款好?五大热门畅销骨传导耳机推荐!
  • 无人机电机损耗!
  • JAVA接口,继承,和抽象类的使用
  • 自动裁剪图像的智能方法:Smart Image Cropping API指南
  • 跨境电商批量自养号测评是怎么做到的?
  • SpringCloud-负载均衡-ribbon
  • 智能优化算法-禁忌搜索算法(TS)(附源码)
  • 服务控制管理器