#220824 接收数据 RX | GRO¶
INDIRECT_CALL_* 宏的作用是什么¶
https://elixir.bootlin.com/linux/latest/source/include/linux/indirect_call_wrapper.h
INDIRECT_CALL_INET(ptype->callbacks.gro_complete,
ipv6_gro_complete, inet_gro_complete, skb, 0);
宏展开之后就是:
likely(ptype->callbacks.gro_complete == inet_gro_complete)?
inet_gro_complete(skb, 0)
:
(likely(ptype->callbacks.gro_complete == ipv6_gro_complete)?
ipv6_gro_complete(skb, 0)
:
ptype->callbacks.gro_complete(skb, 0));
功能就是猜某个函数指针指向的是哪个函数,如果是某几个已知函数,那就直接调用这些函数,而不是通过函数指针。因为 直接调用函数比通过函数指针调用的性能要好 。这个具体可以参见:https://lwn.net/Articles/774743/,大体说来就是:
从 napi_gro_receive 到 netif_receive_skb¶
GRO 全称 Generic Receive Offload,是 Linux 网络栈里收包侧的一个软件优化,功能就是将网卡收到的一堆 mtu 1500 的小包给合并为大包再交给上层协议栈去处理。GRO 位于 tcpdump 等抓包工具的挂载点之前,所以抓包工具看到的 GRO 之后的大包。
调用栈:
napi_gro_receive(napi, skb)
|- gro_result = dev_gro_receive(napi, skb)
| |- bucket = skb_get_hash_raw(skb)
| |- gro_list = &napi->gro_hash[bucket]
| |
| |- if netif_elide_gro(skb->dev)
| | return GRO_NORMAL
| |
| |- gro_list_prepare(&gro_list->list, skb)
| |
| |- pp = ptype->callbacks.gro_receive/inet_gro_receive
| | |- tcp4_gro_receive
| | |- tcp_gro_receive
| | |- skb_gro_receive
| |
| |- if pp:
| | |- napi_gro_complete
| | |- ptype->callbacks.gro_complete/inet_gro_complete
| | | |- tcp4_gro_complete
| | |- gro_normal_one 👈
| |
| |- if NAPI_GRO_CB(skb)->same_flow
| | return GRO_MERGED_FREE
| |
| |- list_add(&skb->list, &gro_list->list)
| |- return GRO_HELD
|
|- napi_skb_finish(napi, skb, gro_result)
|- switch gro_result
|- case GRO_NORMAL: gro_normal_one(napi, skb, 1) 👈
|- case GRO_MERGED_FREE: __kfree_skb(skb)
|- default: return
(GRO 合并包这个函数比较复杂,这里我们只是要大致理清其脉络,所以上面的调用栈经过了一定的简化)
napi_gro_receive
的输入是驱动从网卡 ring buffer 收获并构建出的一个个 skb 结构体,要合并就得缓存,GRO 模块会将收到的 skb 缓存到 napi->gro_hash
中,这是一个大小为 8 的数组,每个元素又分别是个 skb 列表。
struct napi_struct {
//...
#define GRO_HASH_BUCKETS 8
struct gro_list gro_hash[GRO_HASH_BUCKETS]
//...
}
缓存和合并完成继续往上发送包的逻辑如下:
首先,调用
skb_get_hash_raw
获取包的 hash(这个 hash 是驱动从 ring buffer 收包的时候直接从包的 metadata 中获取到并调用skb_set_hash
设置的,也就是网卡直接提供了的),GRO 模块将相同 hash 的 skb 缓存到同一个列表中。napi->gro_hash[bucket]
获取到缓存 skb 列表,这里面可能有和新到来的 skb 属于同一个流的 skb。调用
netif_elide_gro
检查要不要做 GRO,不做的话直接跳过 GRO 调用napi_skb_finish
,最终调用gro_normal_one
将包继续往上层传。除了网卡关闭 GRO 之外,如果网卡上挂载了 Generic XDP 程序,也会跳过 GRO 处理。如果需要做 GRO,则调用
gro_list_prepare
对比新来的 skb 和 缓存 skb 列表里的每一个 skb 的 L2 协议头(mac header)是否一致,给缓存列表中对比一致的 skb 设置NAPI_GRO_CB(skb)->same_flow = 1
标示其可能和新来的 skb 是一个流的。根据上层协议逐级往上调用上面协议层的
gro_receive
函数,比如一个 TCP 包会依次调用inet_gro_receive
->tcp_gro_receive
函数,每一个协议层中会根据自己这一层的 header 继续过滤缓存 skb 列表中可能是同一个流的 skb。最终如果找到同一个流的 skb 缓存。调用skb_gro_receive
合并包。gro_receive
函数最终会返回一个指针,如果不为空,说明有合并后的 skb 需要往上层送了,这个时候需要级联调用gro_complete
函数更新每层协议头中的一些字段(比如 checksum),完成后,调用gro_normal_one
将包继续往上层传。包被合并后对应的 skb 会在
napi_skb_finish
中被释放掉。如果没有找到同一个流的 skb,新的 skb 会被添加到缓存 skb 列表中。
skb 合并的方法是将 新 skb 的线性数据和非线性数据 合并到 老 skb 的非线性数据区 中。合并的时候优先使用 skb_shared_info->frags
数组(新 skb 的线性区如果是页直接映射的,也可以直接合并到里面,详细见: net: make GRO aware of skb->head_frag ),放不下之后再 fallback 使用 skb_shared_info->frag_list
(可以参见前面 skb 文中 线性 skb 和非线性 skb 第一种和第二种结构)。新 skb 的各种协议头会被 skb_pull
到只剩下数据。
gro_normal_one
函数中, skb 会被保存到 napi->rx_list
列表中(skb 文中 线性 skb 和非线性 skb 第三种结构),当列表长度超过阈值 gro_normal_batch
时,调用 gro_normal_list
批量将 skb 往上层送。 从 netif_receive_skb_list_internal
开始 skb 就算出了 GRO 模块了开始协议栈投递了。
gro_normal_one
|- gro_normal_list
|- netif_receive_skb_list_internal
|- __netif_receive_skb_list
|- __netif_receive_skb_list_core
|- __netif_receive_skb_core
|- deliver_skb
References: