Post

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回调完成发包。

文件net/ipv4/devinet.c

主要函数

inet_rtm_newaddr 通过netlink添加IP地址的处理函数。

inet_rtm_deladdr 通过netlink删除IP地址的处理函数。

devinet_ioctl 通过ioctl方式添加或者删除IP地址的处理函数。

文件net/ipv4/ip_input.c

主要函数

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的事件处理流程。

文件net/ipv4/ip_output.c

主要函数

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发包。

文件: net/ipv4/tcp_ipv4.c

主要函数:

tcp_v4_rcvaf_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的快速处理。

文件: net/ipv4/tcp_input.c

主要函数:

tcp_rcv_state_process 根据TCP的不同状态处理TCP报文,当非established状态的时候触发tcp状态机,否则使用tcp_data_queue将报文放入队列中,后续由用户态调用的recv/read调用处理。

文件: net/ipv4/tcp_output.c

主要函数:

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处理。

文件: net/core/neighbour.c

主要函数:

__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_xmitmpls_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的添加,删除,获取, 导出和配置操作。

文件: net/core/neighbour.h

主要函数:

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被ipv4ipv6调用, 注意这没有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中。

This post is licensed under CC BY 4.0 by the author.