这篇主要学习链路层在内核协议栈的实现,包括初始化、注册以及接收发送,会涉及相关函数和代码所在位置。
我们知道以太网不仅可以传输IP分组,还可以传输其他协议的分组,接收系统必须能够区分不同的协议类型,以便将数据转发到正确的例程进一步处理。因为分析数据并查明所用传输协议比较耗时,所以在以太网的帧首部包含了一个标识符,ip数据包的以太类型为0x0800,存在在以太网14字节报头中的前两个字节中。(定义在include/uapi/linux/if_ether.h,#define ETH_P_IP 0x0800 /* Internet Protocol packet */)。这些都是在链路层实现的。在链路层的帧处理由中断事件驱动。中断会将帧复制到sk_buff数据结构中。
那么下面我们补补深入看下。
1.1.1.1 初始化
链路层比较底层,涉及的内容比较多,因为先后逻辑关系比较复杂,现在这些知识点放在一起,后续再逐个剥离之。
我们从start_kernel函数开始,该函数定义在init/main.c中。关于start_kernel的其他工作先不去讲解了,不然容易一环一环无法解套,我们直接将网络初始化相关的内容。
我们只需要知道该函数会调用和网络相关的初始化函数,如下:
init_IRQ();
init_timers();
softirq_init();
完成定时器、硬中断和软中断初始化,然后启动init进程。直接给出一个初始化逻辑流程图。
1.1.1.2 sock_init
从图中我们知道,start_kernel函数最终会调用do_initcalls函数,调用通过xxx_initcall注册的各种函数,而sock_init就是其中之一。
sock_init(net/socket.c)函数放在级别为1的代码中
(core_initcall(sock_init)),用于初始化应用层网络协议。该函数调用skb_init函数,创建skbuff SLAB缓存区。调用init_inodecache函数初始化协议模块。并注册文件系统sock_fs_type。
这个可以参考文章:
1.1.1.3 inet_init
inet_init函数(net/ipv4/af_inet.c)初始化internet 协议族的协议栈。定义如下:
static int __init inet_init(void)
也是__init类的函数, fs_initcall(inet_init);放在优先级为5的代码中。
inet_init函数初始化internet 协议族的协议栈。
1.1.1.4 proto_init
subsys_initcall(proto_init);也是放在优先级为6的代码段中。
调用register_pernet_subsys函数来注册网络命名空间。
1.1.1.5 net_dev_init
函数net_dev_init同理放在优先级给6的代码中。
subsys_initcall(net_dev_init);
net_dev_init函数(定义在net/core/dev.c),在启动的时候调用。单线程执行不需要rtnl信号保护。用于在初始化设备。在开启时候会遍历设备列表,保证都是可用的设备都在线。
其会调用dev_proc_init函数, 它在/proc/net 下注册3个文件。/proc/net/softnet_stat输出netdevice设备的统计信息。例如如下,每行表示一个CPU数据:
06e3cd8a 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
其中第一列为该CPU所接收到的所有数据包。
netdev_kobject_init()函数,在/sys/class/下注册net类 它和设备模型有关.
register_pernet_subsys注册的所有的网络命名空间子系统都加入到 static struct list_head *first_device链表里.
注册软中断,如下:
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
PS:网络设备命名:ethX:表示以太网适配器;pppX表示通过调制解调器简历的链接;isdnX表示ISDN卡;atmX表示异步传输模式,高速网卡的接口;lo环回设备。
1.1.1.6 接收
前面涉及了很多系统初始化本身的故事,虽然有线条没分支,但是也是涉及了不少点,因而将材料名字也做了相应修改。后续会将初始化的内容丰满之。
由于网络包的到达时间是不可预测的,所以所有现代设备驱动程序都使用中断来通知内核有包到达。现在所有的网卡都支持DMA模式,能自行将数据传输到物理内存。
总体逻辑如下:
net_interrupt是由设备驱动程序设置的中断处理程序。如果是分组引发(排除报告错误),则将控制转移到net_rx。net_rx创建一个套接字缓冲区,将包从网卡传输到缓冲区(物理内存),然后分析首部数据,确定包所使用的网络层协议。
然后调用netif_rx,该函数不特定于网络驱动程序。标志着控制从网卡代码转移到了网络层的通用接口部分。
netif_rx函数从设备驱动中接收一个包,将其排队给上层协议中处理。函数总是成功的。包的命运交于协议层处理,比如由于流程控制进行丢弃。将缓存投递到网络代码中。在结束之前将软中断NET_RX_SOFTIRQ(include/linux/interrupt.h)标记为即将执行,然后退出中断上下文。
softnet_data数组管理进出分组的等待队列,每个CPU都会创建等待队列,支持并发处理。softnet_data结构定义在include/linux/netdevice.h.
net_rx_action(net/core/dev.c)用作软中断的处理程序,net_rx_action调用设备的poll方法(默认为process_backlog),process_backlog函数循环处理所有分组。调用__skb_dequeue从等待队列移除一个套接字缓冲区。
调用__netif_receive_skb(net/core/dev.c)函数,分析分组类型、处理桥接,然后调用deliver_skb(net/core/dev.c),该函数调用packet_type->func使用特定于分组类型的处理程序,代码逻辑如下图所示:。
关于NAPI,为了防止中断过快导致出现中断风暴,NAPI采用了IRQ和轮询的组合。实现NAPI的条件是:1.设备能够保留多个接收(例如DMA),2.设备能够禁用用于分组接收的IRQ。
1.1.1.7 发送
发送时候除了特定协议需要完成的首部和校验和,以及由高层协议实例生成的数据之外,分组的路由是最重要的。在一个网卡系统下,内核也要区分发送到外部目标还是针对环回接口。
从网络层下来调用的链路层发送函数是dev_queue_xmit,直接调用__dev_queue_xmit函数,
分组放置到等待队列上一定时间之后,分组将可以发出,通过网卡的定的函数 hard_start_xmit 来完成,在每个 net_device 结构中都以函数指针形式出现,由硬件设备驱动程序实现。