Linux kernel net code reading
断断续续也阅读了 Linux kernel 中网络子系统的一部分代码,主要集中在目录 net 下。主要的工具有 https://elixir.bootlin.com/linux, https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/ 和 https://github.com/torvalds/linux。
具体这几个工具的比较可以参考这里。下面是目前主要阅读的一些源文件,并根据自己的阅读经验总结了主要功能,后面可以集中时间专门总结一下网络子系统。
文件: net/core/dev.c
主要函数:
netif_rx 驱动层收包之后,申请skb后交给网络协议栈的入口。支持在硬件中断中处理报文,实际是通过发送给queue调度处理。
netif_receive_skb 驱动层收包之后,申请skb后交给网络协议栈的入口。和netif_rx的区别是,支持在软中断中处理报文,即直接在当前上下文中处理,一般用在NAPI的poll函数中。
napi_gro_receive 驱动层收包之后,通过napi方式触发poll轮询收包,之后使用该接口发送给网络协议栈。
__napi_schedule 在驱动中断处理中,通过该函数触发NAPI的调度,也就是调用驱动的poll轮询回调。
netif_napi_add 驱动程序通过该接口向napi注册poll回调函数。
napi_complete_done 驱动在poll中完成收包后,通过该接口告诉NAPI完成收包。有部分驱动调用的是napi_complete,其实是封装的napi_complete_done
dev_queue_xmit 驱动层发包函数,这是驱动层对上层提供的统一发包入口,比如neighbour邻居子系统通过调用该函数实现发包。这个函数通过dev_hard_start_xmit->xmit_one->netdev_start_xmit->__netdev_start_xmit调用路径,最终调用具体驱动程序注册的ndo_start_xmit回调完成发包。
主要函数:
inet_rtm_newaddr 通过netlink添加IP地址的处理函数。
inet_rtm_deladdr 通过netlink删除IP地址的处理函数。
devinet_ioctl 通过ioctl方式添加或者删除IP地址的处理函数。
主要函数:
ip_rcv 网络协议栈IP层收包入口,具体是在af_inet.c中注册,在dev.c中被调用。注意在被调用的实现中,使用了 INDIRECT_CALL_INET,而且在参数中可以看到可能被回调的函数ip_rcv或ipv6_rcv,这是一种间接调用方式,可以防止一些潜在攻击。 ip_rcv通过调用ip_rcv_core完成ip头有效性校验,再调用ip_rcv_finish进入收包处理流程。在ip_rcv_finish中,通过ip_rcv_finish_core完成路由查找,具体说是通过ip_route_input_noref决定是上送本机还是进行转发,分别设置dst.input为ip_local_deliver或者ip_forward。最后,在ip_rcv中通过dst_input调用进入上面dst.input的事件处理流程。
主要函数:
ip_output 在rt_dst_alloc中会将dst.output初始化为ip_output, 后面调用dst_output的地方,实际就是进入ip_output中进行处理。而ip_output最终会调用ip_finish_output。ip_finish_output的调用路径图如下:
ip_finish_output
–>__ip_finish_output
—->dst_output NAT策略处理之后,需要重新再走一遍output流程。
—->ip_finish_output_gso gso处理流程
——>ip_finish_output2
——>ip_fragment
—->ip_fragment 需要分片处理的报文
——>ip_finish_output2
—->ip_finish_output2
可见最后都会进入ip_finish_output2,该函数的调用路径如下:
ip_finish_output2
–>ip_neigh_for_gw route.h
—->ip_neigh_gw4
—->ip_neigh_gw6
–>neigh_output neighbour.h
—->neigh_hh_output
—->n->output
ip_queue_xmit tcp层发包最终调用的接口,该接口内部再调用__ip_queue_xmit,后者内部先通过ip_route_output_ports查找路由,最后通过ip_local_out发包。ip_local_out内部最终调用dst_output发包。
主要函数:
tcp_v4_rcv 在af_inet.c中注册的tcp处理函数,根据搜索结果,在ip_protocol_deliver_rcu中被调用,其实是通过handler指针调用的,同时通过INDIRECT_CALL_2实现间接调用。tcp_v4_rcv做完报文有效性检查后,会调用__inet_lookup_skb查找是否有src+dst对应的socket,最后会同步或异步(tcp_add_backlog)调用tcp_v4_do_rcv。
tcp_v4_do_rcv tcp_v4_do_rcv会根据tcp的不同状态进行处理,最终会调用tcp_rcv_state_process处理报文。
tcp_v4_early_demux 在ip_rcv_finish_core中做路由查找之前,当配置了sysctl_ip_early_demux支持通过tcp_v4_early_demux做tcp的早期解复用处理,及tcp的快速处理。
主要函数:
tcp_rcv_state_process 根据TCP的不同状态处理TCP报文,当非established状态的时候触发tcp状态机,否则使用tcp_data_queue将报文放入队列中,后续由用户态调用的recv/read调用处理。
主要函数:
tcp_write_xmit 发送TCP报文的接口,主要提供给__tcp_push_pending_frames和tcp_push_one使用,后两者会在tcp.c中被调用。
tcp_transmit_skb TCP内部及状态机报文发送接口,主要在tcp_output.c内部使用,tcp_write_xmit也是通过这个接口发包的。最终会调用queue_xmit回调完成报文的发送,具体的接口ipv4和ipv6分别对应ip_queue_xmit和inet6_csk_xmit。
文件: net/ipv4/tcp.c
主要函数:
tcp_sendmsg 用户态send/write/sendmsg/sendto最终调用的发送接口,最终调用tcp_sendmsg_locked。
tcp_sendmsg_locked( 实现tcp报文发送,支持零拷贝方式,支持根据MSS分片处理,通过tcp_write_queue_tail读取sk_write_queue,即tcp的发送队列,当push的时候通过__tcp_push_pending_frames或者tcp_push_one完成发包。通过skb_entail将skb放入sk_write_queue,以便实现异步发包。
tcp_recvmsg 用户态recv/read/recvmsg/recvfrom最终调用的接收接口,通过skb_peek_tail从sk_receive_queue获得数据,即tcp的接收队列。
文件: net/ipv4/arp.c
主要函数:
arp_send 向指定目的IP发送arp,type决定的发送报文的类型,支持请求和响应报文。具体通过arp_create创建skb,通过arp_xmit发包。有些模块在发包之前有自己的处理流程,比如bound模块,参考[这里(]https://elixir.bootlin.com/linux/v5.10.70/source/drivers/net/bonding/bond_main.c#L2704),或者vxlan模块,参考这里。所以将组包和发包进行了拆分。
arp_create 创建arp报文,根据输入的参数填充skb中的相关字段结构。
arp_xmit 通过调用arp_xmit_finish发包,而arp_xmit_finish中直接调用了dev_queue_xmit进行发包。
下面是arp的一些处理函数,对于理解arp流程会有重要帮助。
arp_constructor 构建neighbour结构体,也就是arp表项的构建,由neigh邻居子系统在创建neigh的时候调用。
arp_solicit 发送arp请求,内部最终通过arp_send_dst发送。arp_send_dst内部通过arp_create和arp_xmit完成发包。由neigh邻居子系统在发送请求的时候调用。
arp_process 处理接收到的arp报文,包括邻居发送的请求报文以及响应报文。
arp_rcv 向协议栈注册的arp收包处理入口,完成arp报文格式基本检查已经netfilter处理后,交给arp_process处理。
主要函数:
__neigh_create 创建neighbour表项。ipv4和ipv6通过该接口创建neigh表项,请参考这里和这里。在ipv4的流程中,会调用arp中的arp_constructor构建neighbour结构,
neigh_destroy 删除neighbour表项。
neigh_update 更新neighbour状态,由arp或者ipv6 nd根据接收到的报文调用。
neigh_xmit 查找对应地址的neigh表项,如果不存在则创建,然后通过neigh子系统中的output回调进行探测或者发包。可以看出,这是没有 cache 支持的neigh发送流程,什么模块会使用呢: mpls。mpls没有类似于ip转发的hh缓存,所以在mpls_xmit和mpls_forward中都是通过neigh_xmit发送报文的。
neigh_suspect neigh状态可疑的时候,关闭快速发送通道。
neigh_connect neigh状态正常的时候,打开快速发送通道。
neigh_resolve_output 解析neigh状态后发送,通过neigh_event_send->__neigh_event_send触发定时器或者立即发送arp请求,如果dev的header_ops中cache有效,这里对于eth设备对应的是eth_header_cache,同时hh.hh_len为0表示没有缓存,则调用 neigh_hh_init 更新hh缓存,然后根据neigh填充二层头。最后通过dev_queue_xmit发包。
neigh_hh_init 调用 neigh 对应的 dev 中的 header_ops->cache更新 hh 缓存,cache 对于 eth 对应的是 eth_header_cache, 在 ether_setup中通过eth_header_ops注册的。
neigh_connected_output 当前已完成neigh探测后,当没有支持hh cache的时候进行发包。对于eth设备来说,因为支持hh,所有不走这里。这是一种通用接口,一些没有支持邻居缓存的设备使用这种方法完成状态解析后的发包。从 neigh_connected_output 和 neigh_resolve_output 的代码可以看出,前者相对于后者少了缓存的流程。
neigh_direct_output 直接调用dev_queue_xmit进行发包。当dev设备结构中的header_ops没有赋值的时候使用这种发送模式,比如一些虚拟设备。对于eth设备,在ether_setup中进行了赋值。
neigh_parms_alloc neigh参数初始化,同时绑定dev设备和neigh表(arp,nd等)。
neigh_table_init neigh邻居表初始化,arp和ipv6 nd调用初始化arp表和ip nd表。
neigh_add neigh_delete neigh_get neightbl_dump_info neightbl_set 上面5个函数是向rtnetlink注册的回调函数,分别实现neigh的添加,删除,获取, 导出和配置操作。
主要函数:
neigh_output 邻居子系统提供的发送接口,以 inline 的形式定义在 neighbour.h 中。
1
2
3
4
5
6
7
8
9
10
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb,
bool skip_cache)
{
const struct hh_cache *hh = &n->hh;
if ((n->nud_state & NUD_CONNECTED) && hh->hh_len && !skip_cache)
return neigh_hh_output(hh, skb);
else
return n->output(n, skb);
}
当 neigh 的状态为 NUD_CONNECTED,且 h h缓存长度非0, 且未设置 skip_cache,则使用 neigh_hh_output 进行缓存发送,否则使用neigh注册的output回调,其实就是neigh_resolve_output。 neigh_output被ipv4和ipv6调用, 注意这没有mpls, 因此目前mpls没有neigh缓存机制发包。其实bridge在br_nf_pre_routing_finish_bridge中还通过neigh子系统提供的neigh_hh_bridge接口实现了通过缓存机制发包。
neigh_hh_output neigh 子系统提供的缓存发送接口,复制 hh 缓存中的数据到 skb 中,然后使用 dev_queue_xmit 发包。neigh_hh_output目前只被neigh_output调用。 缓存数据是何时初始化的呢?neigh_resolve_output中。