网卡收发包流程浅析(一)(转)

网卡收发包流程浅析(一)--环形缓冲区

Posted by Albert on July 27, 2020

网卡收发包流程浅析(一)–环形缓冲区(转)

一、前言

​ 原文地址:https://ylgrgyq.github.io/2017/07/23/linux-receive-packet-1/

二、环形缓冲区收包

环形缓冲区示意图:

​ 关于环形缓冲区,维基百科上有专门的动画进行讲解—–https://en.wikipedia.org/wiki/Circular_buffer

​ NIC (network interface card) 在系统启动过程中会向系统注册自己的各种信息,系统会分配 Ring Buffer 队列也会分配一块专门的内核内存区域给 NIC 用于存放传输上来的数据包。struct sk_buff 是专门存放各种网络传输数据包的内存接口,在收到数据存放到 NIC 专用内核内存区域后,sk_buff 内有个 data 指针会指向这块内存

​ Ring Buffer 队列内存放的是一个个 Packet Descriptor ,其有两种状态: ready 和 used 。初始时 Descriptor 是空的,指向一个空的 sk_buff,处在 ready 状态。当有数据时,DMA 负责从 NIC 取数据,并在 Ring Buffer 上按顺序找到下一个 ready 的 Descriptor,将数据存入该 Descriptor 指向的 sk_buff 中,并标记槽为 used。因为是按顺序找 ready 的槽,所以 Ring Buffer 是个 FIFO 的队列。

​ 当 DMA 读完数据之后,NIC 会触发一个 IRQ 让 CPU 去处理收到的数据。因为每次触发 IRQ 后 CPU 都要花费时间去处理 Interrupt Handler,如果 NIC 每收到一个 Packet 都触发一个 IRQ 会导致 CPU 花费大量的时间在处理 Interrupt Handler,处理完后又只能从 Ring Buffer 中拿出一个 Packet,虽然 Interrupt Handler 执行时间很短,但这么做也非常低效,并会给 CPU 带去很多负担。所以目前都是采用一个叫做 New API(NAPI) 的机制,去对 IRQ 做合并以减少 IRQ 次数。

​ 接下来介绍一下 NAPI 是怎么做到 IRQ 合并的。它主要是让 NIC 的 driver 能注册一个 poll 函数,之后 NAPI 的 subsystem 能通过 poll 函数去从 Ring Buffer 中批量拉取收到的数据。主要事件及其顺序如下:

  1. NIC driver 初始化时向 Kernel 注册 poll 函数,用于后续从 Ring Buffer 拉取收到的数据
  2. driver 注册开启 NAPI,这个机制默认是关闭的,只有支持 NAPI 的 driver 才会去开启
  3. 收到数据后 NIC 通过 DMA 将数据存到内存
  4. NIC 触发一个 IRQ,并触发 CPU 开始执行 driver 注册的 Interrupt Handler
  5. driver 的 Interrupt Handler 通过 napi_schedule 函数触发 softirq (NET_RX_SOFTIRQ) 来唤醒 NAPI subsystem,NET_RX_SOFTIRQ 的 handler 是 net_rx_action 会在另一个线程中被执行,在其中会调用 driver 注册的 poll 函数获取收到的 Packet
  6. driver 会禁用当前 NIC 的 IRQ,从而能在 poll 完所有数据之前不会再有新的 IRQ
  7. 当所有事情做完之后,NAPI subsystem 会被禁用,并且会重新启用 NIC 的 IRQ
  8. 回到第三步

    从上面的描述可以看出来还缺一些东西,Ring Buffer 上的数据被 poll 走之后是怎么交付上层网络栈继续处理的呢?以及被消耗掉的 sk_buff 是怎么被重新分配重新放入 Ring Buffer 的呢?

这两个工作都在 poll 中完成,上面说过 poll 是个 driver 实现的函数,所以每个 driver 实现可能都不相同。但 poll 的工作基本是一致的就是:

  1. 从 Ring Buffer 中将收到的 sk_buff 读取出来
  2. 对 sk_buff 做一些基本检查,可能会涉及到将几个 sk_buff 合并因为可能同一个 Frame 被分散放在多个 sk_buff 中
  3. 将 sk_buff 交付上层网络栈处理
  4. 清理 sk_buff,清理 Ring Buffer 上的 Descriptor 将其指向新分配的 sk_buff 并将状态设置为 ready
  5. 更新一些统计数据,比如收到了多少 packet,一共多少字节等

如果拿 intel igb 这个网卡的实现来看,其 poll 函数在这里:linux/drivers/net/ethernet/intel/igb/igb_main.c - Elixir - Free Electrons

首先是看到有 tx.ring 和 rx.ring,说明收发消息都会走到这里。发消息先不管,先看收消息,收消息走的是 igb_clean_rx_irq。收完消息后执行 napi_complete_done 退出 polling 模式,并开启 NIC 的 IRQ。从而我们知道大部分工作是在 igb_clean_rx_irq 中完成的,其实现大致上还是比较清晰的,就是上面描述的几步。里面有个 while 循环通过 buget 控制,从而在 Packet 特别多的时候不要让 CPU 在这里无穷循环下去,要让别的事情也能够被执行。循环内做的事情如下:

  1. 先批量清理已经读出来的 sk_buff 并分配新的 buffer 从而避免每次读一个 sk_buff 就清理一个,很低效
  2. 找到 Ring Buffer 上下一个需要被读取的 Descriptor ,并检查描述符状态是否正常
  3. 根据 Descriptor 找到 sk_buff 读出来
  4. 检查是否是 End of packet,是的话说明 sk_buff 内有 Frame 的全部内容,不是的话说明 Frame 数据比 sk_buff 大,需要再读一个 sk_buff,将两个 sk_buff 数据合并起来
  5. 通过 Frame 的 Header 检查 Frame 数据完整性,是否正确之类的
  6. 记录 sk_buff 的长度,读了多少数据
  7. 设置 Hash、checksum、timestamp、VLAN id 等信息,这些信息是硬件提供的。
  8. 通过 napi_gro_receive 将 sk_buff 交付上层网络栈
  9. 更新一堆统计数据
  10. 回到 1,如果没数据或者 budget 不够就退出循环

看到 budget 会影响到 CPU 执行 poll 的时间,budget 越大当数据包特别多的时候可以提高 CPU 利用率并减少数据包的延迟。但是 CPU 时间都花在这里会影响别的任务的执行。

budget 默认 300,可以调整
sysctl -w net.core.netdev_budget=600

napi_gro_receive会涉及到 GRO 机制,稍后再说,大致上就是会对多个数据包做聚合,napi_gro_receive 最终是将处理好的 sk_buff 通过调用 netif_receive_skb,将数据包送至上层网络栈。执行完 GRO 之后,基本可以认为数据包正式离开 Ring Buffer,进入下一个阶段了。在记录下一阶段的处理之前,补充一下收消息阶段 Ring Buffer 相关的更多细节。

三、Generic Receive Offloading(GRO)

​ GRO 是 Large receive offload 的一个实现。网络上大部分 MTU 都是 1500 字节,开启 Jumbo Frame 后能到 9000 字节,如果发送的数据超过 MTU 就需要切割成多个数据包。LRO 就是在收到多个数据包的时候将同一个 Flow 的多个数据包按照一定的规则合并起来交给上层处理,这样就能减少上层需要处理的数据包数量。

​ 很多 LRO 机制是在 NIC 上实现的,没有实现 LRO 的 NIC 就少了上述合并数据包的能力。而 GRO 是 LRO 在软件上的实现,从而能让所有 NIC 都支持这个功能。

napi_gro_receive 就是在收到数据包的时候合并多个数据包用的,如果收到的数据包需要被合并,napi_gro_receive 会很快返回。当合并完成后会调用 napi_skb_finish ,将因为数据包合并而不再用到的数据结构释放。最终会调用到 netif_receive_skb 将数据包交到上层网络栈继续处理。netif_receive_skb 上面说过,就是数据包从 Ring Buffer 出来后到上层网络栈的入口。

可以通过 ethtool 查看和设置 GRO:

查看 GRO
ethtool -k eth0 | grep generic-receive-offload
generic-receive-offload: on
设置开启 GRO
ethtool -K eth0 gro on

三、多 CPU 下的 Ring Buffer 处理 (Receive Side Scaling)

​ NIC 收到数据的时候产生的 IRQ 只可能被一个 CPU 处理,从而只有一个 CPU 会执行 napi_schedule 来触发 softirq,触发的这个 softirq 的 handler 也还是会在这个产生 softIRQ 的 CPU 上执行。所以 driver 的 poll 函数也是在最开始处理 NIC 发出 IRQ 的那个 CPU 上执行。于是一个 Ring Buffer 上同一个时刻只有一个 CPU 在拉取数据。

​ 从上面描述能看出来分配给 Ring Buffer 的空间是有限的,当收到的数据包速率大于单个 CPU 处理速度的时候 Ring Buffer 可能被占满,占满之后再来的新数据包会被自动丢弃。而现在机器都是有多个 CPU,同时只有一个 CPU 去处理 Ring Buffer 数据会很低效,这个时候就产生了叫做 Receive Side Scaling(RSS) 或者叫做 multiqueue 的机制来处理这个问题。WIKI 对 RSS 的介绍挺好的,简洁干练可以看看: Network interface controller - Wikipedia

​ 简单说就是现在支持 RSS 的网卡内部会有多个 Ring Buffer,NIC 收到 Frame 的时候能通过 Hash Function 来决定 Frame 该放在哪个 Ring Buffer 上,触发的 IRQ 也可以通过操作系统或者手动配置 IRQ affinity 将 IRQ 分配到多个 CPU 上。这样 IRQ 能被不同的 CPU 处理,从而做到 Ring Buffer 上的数据也能被不同的 CPU 处理,从而提高数据的并行处理能力。

​ RSS 除了会影响到 NIC 将 IRQ 发到哪个 CPU 之外,不会影响别的逻辑了。收消息过程跟之前描述的是一样的。

​ 如果支持 RSS 的话,NIC 会为每个队列分配一个 IRQ,通过 /proc/interrupts 能进行查看。你可以通过配置 IRQ affinity 指定 IRQ 由哪个 CPU 来处理中断。先通过 /proc/interrupts 找到 IRQ 号之后,将希望绑定的 CPU 号写入 /proc/irq/IRQ_NUMBER/smp_affinity,写入的是 16 进制的 bit mask。比如看到队列 rx_0 对应的中断号是 41 那就执行:

echo 6 > /proc/irq/41/smp_affinity
6 表示的是 CPU2 和 CPU1

​ 0 号 CPU 的掩码是 0x1 (0001),1 号 CPU 掩码是 0x2 (0010),2 号 CPU 掩码是 0x4 (0100),3 号 CPU 掩码是 0x8 (1000) 依此类推。

另外需要注意的是设置 smp_affinity 的话不能开启 irqbalance 或者需要为 irqbalance 设置 –banirq 列表,将设置了 smp_affinity 的 IRQ 排除。不然 irqbalance 机制运作时会忽略你设置的 IRQ affinity 配置。

Receive Packet Steering(RPS) 是在 NIC 不支持 RSS 时候在软件中实现 RSS 类似功能的机制。其好处就是对 NIC 没有要求,任何 NIC 都能支持 RPS,但缺点是 NIC 收到数据后 DMA 将数据存入的还是一个 Ring Buffer,NIC 触发 IRQ 还是发到一个 CPU,还是由这一个 CPU 调用 driver 的 poll 来将 Ring Buffer 的数据取出来。RPS 是在单个 CPU 将数据从 Ring Buffer 取出来之后才开始起作用,它会为每个 Packet 计算 Hash 之后将 Packet 发到对应 CPU 的 backlog 中,并通过 Inter-processor Interrupt(IPI) 告知目标 CPU 来处理 backlog。后续 Packet 的处理流程就由这个目标 CPU 来完成。从而实现将负载分到多个 CPU 的目的。

​ RPS 默认是关闭的,当机器有多个 CPU 并且通过 softirqs 的统计 /proc/softirqs 发现 NET_RX 在 CPU 上分布不均匀或者发现网卡不支持 mutiqueue 时,就可以考虑开启 RPS。开启 RPS 需要调整 /sys/class/net/DEVICE_NAME/queues/QUEUE/rps_cpus 的值。比如执行:

echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus

​ 表示的含义是处理网卡 eth0 的 rx-0 队列的 CPU 数设置为 f 。即设置有 15 个 CPU 来处理 rx-0 这个队列的数据,如果你的 CPU 数没有这么多就会默认使用所有 CPU 。甚至有人为了方便都是直接将 echo fff > /sys/class/net/eth0/queues/rx-0/rps_cpus 写到脚本里,这样基本能覆盖所有类型的机器,不管机器 CPU 数有多少,都能覆盖到。从而就能让这个脚本在任意机器都能执行。

注意:如果 NIC 不支持 mutiqueue,RPS 不是完全不用思考就能打开的,因为其开启之后会加重所有 CPU 的负担,在一些场景下比如 CPU 密集型应用上并不一定能带来好处。所以得测试一下。

Receive Flow Steering(RFS) 一般和 RPS 配合一起工作。RPS 是将收到的 packet 发配到不同的 CPU 以实现负载均衡,但是可能同一个 Flow 的数据包正在被 CPU1 处理,但下一个数据包被发到 CPU2,会降低 CPU cache hit 比率并且会让数据包要从 CPU1 发到 CPU2 上。RFS 就是保证同一个 flow 的 packet 都会被路由到正在处理当前 Flow 数据的 CPU,从而提高 CPU cache 比率。这篇文章 把 RFS 机制介绍的挺好的。基本上就是收到数据后根据数据的一些信息做个 Hash 在这个 table 的 entry 中找到当前正在处理这个 flow 的 CPU 信息,从而将数据发给这个正在处理该 Flow 数据的 CPU 上,从而做到提高 CPU cache hit 率,避免数据在不同 CPU 之间拷贝。当然还有很多细节,请看上面链接。

RFS 默认是关闭的,必须主动配置才能生效。正常来说开启了 RPS 都要再开启 RFS,以获取更好的性能。这篇文章也有说该怎么去开启 RFS 以及推荐的配置值。一个是要配置 rps_sock_flow_entries

sysctl -w net.core.rps_sock_flow_entries=32768

这个值依赖于系统期望的活跃连接数,注意是同一时间活跃的连接数,这个连接数正常来说会大大小于系统能承载的最大连接数,因为大部分连接不会同时活跃。该值建议是 32768,能覆盖大多数情况,每个活跃连接会分配一个 entry。除了这个之外还要配置 rps_flow_cnt,这个值是每个队列负责的 flow 最大数量,如果只有一个队列,则 rps_flow_cnt 一般是跟 rps_sock_flow_entries 的值一致,但是有多个队列的时候 rps_flow_cnt 值就是 rps_sock_flow_entries / N, N 是队列数量。

echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

Accelerated Receive Flow Steering (aRFS) 类似 RFS 只是由硬件协助完成这个工作。aRFS 对于 RFS 就和 RSS 对于 RPS 一样,就是把 CPU 的工作挪到了硬件来做,从而不用浪费 CPU 时间,直接由 NIC 完成 Hash 值计算并将数据发到目标 CPU,所以快一点。NIC 必须暴露出来一个 ndo_rx_flow_steer 的函数用来实现 aRFS。

四、adaptive RX/TX IRQ coalescing

​ 有的 NIC 支持这个功能,用来动态的将 IRQ 进行合并,以做到在数据包少的时候减少数据包的延迟,在数据包多的时候提高吞吐量。查看方法:

ethtool -c eth1
Coalesce parameters for eth1:
Adaptive RX: off  TX: off
stats-block-usecs: 0
.....

开启 RX 队列的 adaptive coalescing 执行:

ethtool -C eth0 adaptive-rx on

并且有四个值需要设置:rx-usecs、rx-frames、rx-usecs-irq、rx-frames-irq,具体含义等需要用到的时候查吧。

五、Ring Buffer 相关监控及配置

收到数据包统计

ethtool -S eh0
NIC statistics:
     rx_packets: 792819304215
     tx_packets: 778772164692
     rx_bytes: 172322607593396
     tx_bytes: 201132602650411
     rx_broadcast: 15118616
     tx_broadcast: 2755615
     rx_multicast: 0
     tx_multicast: 10

RX 就是收到数据,TX 是发出数据。还会展示 NIC 每个队列收发消息情况。其中比较关键的是带有 drop 字样的统计和 fifo_errors 的统计 :

tx_dropped: 0
rx_queue_0_drops: 93
rx_queue_1_drops: 874
....
rx_fifo_errors: 2142
tx_fifo_errors: 0

看到发送队列和接收队列 drop 的数据包数量显示在这里。并且所有 queue_drops 加起来等于 rx_fifo_errors。所以总体上能通过 rx_fifo_errors 看到 Ring Buffer 上是否有丢包。如果有的话一方面是看是否需要调整一下每个队列数据的分配,或者是否要加大 Ring Buffer 的大小。

/proc/net/dev是另一个数据包相关统计,不过这个统计比较难看:

cat /proc/net/dev
Inter-|   Receive                                                |  Transmit
 face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
    lo: 14472296365706 10519818839    0    0    0     0          0         0 14472296365706 10519818839    0    0    0     0       0          0
  eth1: 164650683906345 785024598362    0    0 2142     0          0         0 183711288087530 704887351967    0    0    0     0       0          0

调整 Ring Buffer 队列数量

ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX:             0
TX:             0
Other:          1
Combined:       8
Current hardware settings:
RX:             0
TX:             0
Other:          1
Combined:       8

看的是 Combined 这一栏是队列数量。Combined 按说明写的是多功能队列,猜想是能用作 RX 队列也能当做 TX 队列,但数量一共是 8 个?

如果不支持 mutiqueue 的话上面执行下来会是:

Channel parameters for eth0:
Cannot get device channel parameters
: Operation not supported

看到上面 Ring Buffer 数量有 maximums 和 current settings,所以能自己设置 Ring Buffer 数量,但最大不能超过 maximus 值:

sudo ethtool -L eth0 combined 8

如果支持对特定类型 RX 或 TX 设置队列数量的话可以执行:

sudo ethtool -L eth0 rx 8

需要注意的是,ethtool 的设置操作可能都要重启一下才能生效。

调整 Ring Buffer 队列大小

先查看当前 Ring Buffer 大小:

ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX:   4096
RX Mini:  0
RX Jumbo: 0
TX:   4096
Current hardware settings:
RX:   512
RX Mini:  0
RX Jumbo: 0
TX:   512

看到 RX 和 TX 最大是 4096,当前值为 512。队列越大丢包的可能越小,但数据延迟会增加

设置 RX 队列大小:

ethtool -G eth0 rx 4096

调整 Ring Buffer 队列的权重

NIC 如果支持 mutiqueue 的话 NIC 会根据一个 Hash 函数对收到的数据包进行分发。能调整不同队列的权重,用于分配数据。

ethtool -x eth0
RX flow hash indirection table for eth0 with 8 RX ring(s):
    0:      0     0     0     0     0     0     0     0
    8:      0     0     0     0     0     0     0     0
   16:      1     1     1     1     1     1     1     1
   ......
   64:      4     4     4     4     4     4     4     4
   72:      4     4     4     4     4     4     4     4
   80:      5     5     5     5     5     5     5     5
   ......
  120:      7     7     7     7     7     7     7     7

我的 NIC 一共有 8 个队列,一个有 128 个不同的 Hash 值,上面就是列出了每个 Hash 值对应的队列是什么。最左侧 0 8 16 是为了能让你快速的找到某个具体的 Hash 值。比如 Hash 值是 76 的话我们能立即找到 72 那一行:”72: 4 4 4 4 4 4 4 4”,从左到右第一个是 72 数第 5 个就是 76 这个 Hash 值对应的队列是 4 。

ethtool -X eth0 weight 6 2 8 5 10 7 1 5

设置 8 个队列的权重。加起来不能超过 128 。128 是 indirection table 大小,每个 NIC 可能不一样。

更改 Ring Buffer Hash Field

分配数据包的时候是按照数据包内的某个字段来进行的,这个字段能进行调整。

ethtool -n eth0 rx-flow-hash tcp4
TCP over IPV4 flows use these fields for computing Hash flow key:
IP SA
IP DA
L4 bytes 0 & 1 [TCP/UDP src port]
L4 bytes 2 & 3 [TCP/UDP dst port]

查看 tcp4 的 Hash 字段。

也可以设置 Hash 字段:

ethtool -N eth0 rx-flow-hash udp4 sdfn

sdfn 需要查看 ethtool 看其含义,还有很多别的配置值。

softirq 数统计

通过 /proc/softirqs 能看到每个 CPU 上 softirq 数量统计:

cat /proc/softirqs
                    CPU0       CPU1       
          HI:          1          0
       TIMER: 1650579324 3521734270
      NET_TX:   10282064   10655064
      NET_RX: 3618725935       2446
       BLOCK:          0          0
BLOCK_IOPOLL:          0          0
     TASKLET:      47013      41496
       SCHED: 1706483540 1003457088
     HRTIMER:    1698047   11604871
         RCU: 4218377992 3049934909

看到 NET_RX 就是收消息时候触发的 softirq,一般看这个统计是为了看看 softirq 在每个 CPU 上分布是否均匀,不均匀的话可能就需要做一些调整。比如上面看到 CPU0 和 CPU1 两个差距很大,原因是这个机器的 NIC 不支持 RSS,没有多个 Ring Buffer。开启 RPS 后就均匀多了。

IRQ 统计

/proc/interrupts 能看到每个 CPU 的 IRQ 统计。一般就是看看 NIC 有没有支持 multiqueue 以及 NAPI 的 IRQ 合并机制是否生效。看看 IRQ 是不是增长的很快。

六、扩展

​ 在查看NAPI机制的时候发现一篇介绍NAPI引入初衷的文章写的很好,通俗易懂,就想要分享下,重要的是博主还做了可以在他基础上任意修改,而并不用注明出处的声明,着实令我敬佩,不过还是附上原文链接!

​ http://blog.csdn.net/dog250/article/details/5302853

​ 处理外部事件是cpu必须要做的事,因为cpu和外设的不平等性导致外设的事件被cpu 当作是外部事件,其实它们是平等的,只不过冯氏机器不这么认为罢了,既然要处理外部事件,那么就需要一定的方法,方法不止一种,大致有中断和轮询以及一种 混杂又复杂的方式,也就是DMA方式。中断是cpu被动处理的一种方式,也就是说cpu不知道何时中断,只要有了中断就会通知cpu,而cpu此时必须停 下一切来处理,而轮询是cpu主动查询并处理的过程,cpu隔一会查询一下外设看有没有事情可做。 我们看一下这两种方式,中断看似很高效,但是却会遗漏一些数据,避免遗漏的机制要么由硬件实现要么由上层的软件实现,而轮询就没有中断高效了,它会做很多 徒劳的操作,而且必须引入暂存机制,就是说由于cpu不可能在每次查询硬件的时候正好有事情可做,为了不使请求遗漏,随时到来的请求必须暂存在一个私有的 区域内,只要这些都做好了,轮询是不会造成请求遗漏的,中断在很多中断高频触发的时候会造成大量遗漏和竞争,毕竟只有一个cpu,同一个时间点只能有一个 请求被处理,而轮询由于是cpu分批打包处理请求的,因此不会遗漏。 ​ 以上的论述有点像我讨论过的inotify和rsync实现的文件同步,inotify的实现就是中断,很显然有遗漏,而rsync实现的就是轮询,显然 没有遗漏,cpu主动做的事情它自己最明白了,但是它要是被动应对就不会这么明白了,它只是按照规则应对罢了,丝毫不会存在任何策略。如果中断过于频繁也 是不好的,因为cpu必须处理中断,这会导致cpu没有时间做正经事,此时最好用轮询,但是外设活动很缓和的时候,用轮询就不合适了,因为询也是白询,此 时比较适合用中断,可是系统怎么知道何时外设活跃何时外设缓和呢?啊哈,可以用智能预测算法嘛,以历史值为依据!不,不能那样的,因为这是在内核,内核不 是秀算法的地方,我另外的文章强调过这一点。那么怎么办?好办,还是约定,就是将中断和轮询相结合,这就是linux网卡驱动中的NAPI的方式,它的设 计十分巧妙,就是在第一个包到来的时候中断,然后关闭中断开始轮询,等某一次轮询完毕后发现没有数据了,那么内核默认此次数据已经传输完毕,短时间内不会 再有数据了,那么停止轮询,重新开启中断,这样会减少很多次的中断,虽然某次轮询完毕发现没有数据并不能代表1ms以后不会再有数据,但是刚才说了,要想 使算法简单,必须做一个合理的约定,人性化的约定,如果说加上判定什么情况下百分之九十五的可能不需要轮询了并不是不可能,只是维护那个算法的开销太大, 它直接抵消了算法带来的优势。用人的思想考虑,如果一个饭店的服务员不停的从厨房接菜然后送到餐桌,注意是不停的,10秒一趟,但是突然隔了半分钟没有厨 房的人吆喝接菜,如果你是服务员,难道你还会去窗口等菜吗?反正我不会,我会蹲下来稍微休息一下,即使刚蹲下来就会有新活我也愿意赌一把,虽然输得可能性 很大很大。 如此一来,我们看一下NAPI解决了什么问题,第一,它限制了中断的数量,一旦有中断过来就停掉中断改为轮询,这样就不会造成cpu被频繁中断,第 二,cpu不会做无用功,就是所谓的无用的轮询,因为只有在中断来了才改为轮询,中断来了说明有事可做,看看NAPI将中断和轮询结合的是多么巧妙啊。以 往的实现中,在硬件网卡中断中将skb排入队,然后在软中断中出队并交由上层处理,一切配合的看起来那么好,可是在遇到突发快速小包传输的时候就会导致频 繁中断,因为是突发的包,因此不能用轮询,因为是快速小包,因此不适合用中断,最终二者巧妙结合,各取优势,优势互补,绝了!这个框架适合一切的网卡模 式,因此就将传统的网卡收发机制也纳入到了NAPI框架,很简单,就是用原来的逻辑实现dev的poll回调函数即可,至于传统的非NAPI方案,完全可 以用一个桩子代替。 cpu利用率和频繁的中断问题通过NAPI机制解决了,但是这又引入了一个新的问题,就是这可能造成cpu利用率的失衡,这个怎么理解呢?NAPI启动之 后,网卡的中断就会变得很少,要知道中断balance的目前实现是基于中断数量的均衡,它根本不管中断数量均衡之后引起的softirq导致的cpu使 用率是否均衡,softirq的均衡也是一样,比如一个是磁盘softirq,一个是网卡的NAPI的softirq,前者瞬间就可以完成但是来得频繁, 而后者要轮询网卡并且处理协议栈很耗时但是来得不频繁,可是balancer不管这么多,它只是觉得磁盘的softirq太多了而为了数量均衡会将更多的 softirq发布到softiqr少的cpu上,它根本不在乎这些更多的softirq是否会带来更高的cpu负载。NAPI削弱了中断/软中断均衡的 作用,毕竟它的主导在轮询,轮询会占用很多的处理器资源,而中断和软中断数量很少。中断或者软中断特别是软中断数量在cpu间的均衡可能造成各个cpu负 载的严重不均衡,因为各个硬中断几乎都是瞬间完成的,硬中断不能耽搁太久的,但是各个不同软中断的任务量缺是千差万别的,因此绝对不能按照数量来均衡软中 断,然而一般都是硬中断触发软中断,它们都在同一个cpu上,因此如果想简单的实现NAPI在多cpu上的cpu使用率均衡,那么必须重新实现硬件的负载 均衡机制,这样可以吗?不!因此这样会使得两个部分耦合过重,因此必须让硬中断的均衡和cpu的均衡解耦合,其实现在的内核就是这么做的,所以才会造成 cpu不均衡,硬件中断的均衡和cpu均衡的解耦合带来的好处就是我们可以对软中断均衡做文章,而硬中断的负载均衡还是用数量均衡实现,软中断彻底从硬件 中断中解放出来,不再是在处理硬中断的cpu上触发软中断,而是可以在任何cpu上触发软中断,由于不同软中断的任务量千差万别,因此我们定义一个软中断 的“权值”,然后按照不同软中断这个权值和数量的积的和来均衡软中断,这样的话,我想各个cpu的负载就均衡了,现在问题是,各个不同的软中断的“权值” 的计算问题,呵呵。累了,有时间再说。一个论坛上一哥们儿写了一个patch,很有创意,比我这里的软中断均衡的粒度要小得多,这个补丁不是均衡软中断, 而是将软中断进一步也分成了上下两部分,和cpu相关的上半部必须加急处理,这样不会对cpu造成太大负载,仍然用硬件中断均衡,因为硬件中断的cpu触 发软件中断,这部分不变,但是软中断的下半部就需要均衡了,该补丁为每一个cpu创立了一个工作队列,然后将ip_rcv 这种操作的cpu相关的工作放到软中断的上半部,其实就是从一个cpu的skb队列中抽取一个skb,然后将这个skb随机放到这些工作队列中进行处理, 和整个软中断均衡有何不同吗?大大不同。软中断均衡针对的是一个poll_list里面的所有的skb,而这哥们儿的补丁针对的是一个skb,粒度十分 小,但是没有测试,是不是太小了呢?这其实也是一个模式方法,逐步的将粒度精细化,类似将中断分成上半部和下半部的做法是放之四海而皆准的,这是一种哲 学,也是一种风格。 ​ 如果你说你没有见过linux的方式,那么只要你上过枯燥的计算机课或者读过枯燥的教科书或者你是天才你就知道一个叫做生产者/消费者的模型,它其实和 linux的中断的上半部和下半部很类似,上半部是生产者,只管将环境搭建好,数据准备好,然后触发下半部,其实就是唤醒消费者,这个思想在多线程并发中 很著名,而并发就是为了提高系统吞吐量,在SMP环境中也是为了并发,因此我们何尝不用用生产者/消费者模型呢?它也是一种低耦合的各司其职的模型。如果 你想不到NAPI的中断+轮询的方式,那么你听说过linux下怎样做文件同步的吗?rsync+inotify的方式听说过吗?如果没有就赶快 google一下吧。rsync+inotify其实就是中断+轮询,rsync是轮询,而inotify是中断,这个同步方案十分高效,保证只有在 inotify监控到文件变化的时候才开始轮询,平时就睡觉,inotify不再需要监控到具体的文件,因为它只负责告知事件,具体工作由rsync完 成,inotify只需要告诉一端文件变化了即可,那岂不是要全部同步了即使你只改了一个字符,别忘了rsync的算法,这就是另一篇文章了。所以不要再 觉得linux内核深不可测了,它的特点只有一个就是简单,比起用户应用那些复杂的算法,内核的算法一向简单易懂,其实内核的每一个机制,都可以在用户空 间找到原型的。 ​ 可是cpu对NAPI处理的均衡真的有意义吗?用户难道就不能忍受一个cpu占用100%而另一个0%吗?表面上看是那样的,但是如果考虑cache或者 切换代价的话就不一样了,性能因素不仅仅是cpu使用率还有别的,如果一件事的开销过大,即使cpu使用率再均衡也是划不来的。这就好像初学者用free 命令看内存时总是吓一大跳。特别是NAPI的网络数据包操作,比如TCP的IP包的分段重组问题,一旦乱序就要重传,这种情况下,一个linux主机如果只是作为一台路由器的话,那 么进入系统的一个TCP包的不同分段如果被不同的cpu处理并向一个网卡转发了,那么同步问题会很麻烦的,如果你不做同步处理,那么很可能后面的段被一个 cpu先发出去了,那么在真正的接收方接收到乱序的包后就会请求重发,这是不希望的,因此还是一个cpu串行处理好,这也许是TCP/IP协议栈的缺陷, 但是没有办法,协议是那样的,只能那样去应对。在大流量下,由于napi的缘故,网卡的中断被关闭了,此时那个第一次被中断的cpu正在poll这个网卡,因此所有的流量都会聚集到这个cpu上,这可能是一个设计的缺陷吧。