Linux协议栈(6)——初始化及链路层实现

这篇主要学习链路层在内核协议栈的实现,包括初始化、注册以及接收发送,会涉及相关函数和代码所在位置。

我们知道以太网不仅可以传输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进程。直接给出一个初始化逻辑流程图。

Linux协议栈(6)——初始化及链路层实现_第1张图片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

这个可以参考文章:

linux网络驱动初始化module_init函数跟踪》

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.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模式,能自行将数据传输到物理内存。

总体逻辑如下:

Linux协议栈(6)——初始化及链路层实现_第2张图片

net_interrupt是由设备驱动程序设置的中断处理程序。如果是分组引发(排除报告错误),则将控制转移到net_rxnet_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使用特定于分组类型的处理程序,代码逻辑如下图所示:。

Linux协议栈(6)——初始化及链路层实现_第3张图片

关于NAPI,为了防止中断过快导致出现中断风暴,NAPI采用了IRQ和轮询的组合。实现NAPI的条件是:1.设备能够保留多个接收(例如DMA,2.设备能够禁用用于分组接收的IRQ

1.1.1.7 发送

发送时候除了特定协议需要完成的首部和校验和,以及由高层协议实例生成的数据之外,分组的路由是最重要的。在一个网卡系统下,内核也要区分发送到外部目标还是针对环回接口。

从网络层下来调用的链路层发送函数是dev_queue_xmit,直接调用__dev_queue_xmit函数,

分组放置到等待队列上一定时间之后,分组将可以发出,通过网卡的定的函数hard_start_xmit来完成,在每个net_device结构中都以函数指针形式出现,由硬件设备驱动程序实现。


你可能感兴趣的:(Linux,网络,linux网络协议栈)