本文先大致阐述系统协议栈初始化过程,然后剖析数据包的接收和发送通道过程,在文章最后着重梳理其过程及通道结构区别。
源码版本:Linux kernel 1.2.13;工具:Source Insight 3.5
下图为网络协议栈初始化程序流程框架
本篇幅将根据上图来介绍系统网络协议栈的初始化过程。
先从init/main.c 文件出发,在执行了一系列涉及到具体处理器架构初始化代码之后,最终将进入到 init/main.c 中的 start_kernel 函数执行。该函数内部将调用各个初始化程序,我们这里只关注网络栈的初始化过程:系统网络栈初始化总入口函数即为 sock_init 函数,网络栈的初始化将从该函数出发完成。
sock_init 函数定义在 net/socket.c 中,其调用:
1> proto_init 函数:进行协议实现模块初始化;
2> dev_init 函数:进行网络设备驱动层模块初始化;
3> 初始化专门用于网络处理的下半部分执行函数:net_bh,并使能;
4> 将由变量pops指向的域操作函数集合(如INET域,UNIX域)数组清零,在此后的网络栈初始化中会对其进行正确的初始化。
前面的学习我们了解到,不同的域对应不同的域操作函数。
//系统网络栈初始化
void sock_init(void)
{
int i;
printk("Swansea University Computer Society NET3.019\n");
/* * Initialize all address (protocol) families. */
//pops指向的域操作函数集合(如INET域,UNIX域等)数组清零,
//此后的网络栈初始化中会对其进行正确的初始化
for (i = 0; i < NPROTO; ++i) pops[i] = NULL;
/* * Initialize the protocols module. */
//协议实现模块初始化
proto_init();
#ifdef CONFIG_NET
/* * Initialize the DEV module. */
//网络设备驱动层模块初始化
dev_init();
/* * And the bottom half handler */
//初始化专门用于网络处理的下本部分执行函数:net_bh,并使能。
bh_base[NET_BH].routine= net_bh;
enable_bh(NET_BH);
#endif
}
好,接下来看看 proto_init 函数,该函数定义在 net/socket.c 中,其主要完成一下工作:
遍历由 protocols 全局变量(net/protocols.c)指向的域初始化函数集,进行各域的初始化。INET域就初始化为 inet_proto_init,其将在 proto_init 函数被调用以进行 INET 的初始化,UNIX 域的初始化函数为 unix_proto_init。当然还涉及其余域的初始化。
//协议实现模块初始化
void proto_init(void)
{
extern struct net_proto protocols[]; /* Network protocols */
struct net_proto *pro;
/* Kick all configured protocols. */
//遍历由protocols全局变量指向的域初始化函数集,
//进行各域的初始化
pro = protocols;
while (pro->name != NULL)
{
(*pro->init_func)(pro);
pro++;
}
/* We're all done... */
}
dev_init 函数则是对系统中存在的所有网络设备进行初始化操作,其实际上是由具体硬件驱动程序进行初始化,函数实现是内部调用 device 结构中 init 字段指向的函数完成各自特定硬件设备的初始化工作,如果初始化失败,该设备将从系统设备列表中(dev_base指向)删除。
/* * Initialize the DEV module. At boot time this walks the device list and * unhooks any devices that fail to initialise (normally hardware not * present) and leaves us with a valid list of present and active devices. * * The PCMCIA code may need to change this a little, and add a pair * of register_inet_device() unregister_inet_device() calls. This will be * needed for ethernet as modules support. */
void dev_init(void)
{
struct device *dev, *dev2;
/* * Add the devices. * If the call to dev->init fails, the dev is removed * from the chain disconnecting the device until the * next reboot. */
dev2 = NULL;
for (dev = dev_base; dev != NULL; dev=dev->next)
{
if (dev->init && dev->init(dev))
{
/* * It failed to come up. Unhook it. */
if (dev2 == NULL)
dev_base = dev->next;
else
dev2->next = dev->next;
}
else
{
dev2 = dev;
}
}
}
回到proto_init 函数,前面说到该函数的功能是进行各域的初始化。这里我们着重介绍INET域的初始化。
初始化INET域,则是通过调用 inet_proto_init 函数来实现的,参见注释(net/inet/af_inet.c)
/* * Called by socket.c on kernel startup. */
//INET域初始化
void inet_proto_init(struct net_proto *pro)
{
struct inet_protocol *p;
int i;
printk("Swansea University Computer Society TCP/IP for NET3.019\n");
/* * Tell SOCKET that we are alive... */
//注册一个域操作集
(void) sock_register(inet_proto_ops.family, &inet_proto_ops);
seq_offset = CURRENT_TIME*250;
/* * Add all the protocols. */
//初始化INET下所有协议类型
//这些变量都是proto结构体类型,不同协议有对应的proto变量
//proto结构体内包含操作函数集,套接字散列表,以及使用标志字段等
for(i = 0; i < SOCK_ARRAY_SIZE; i++)
{
tcp_prot.sock_array[i] = NULL;
udp_prot.sock_array[i] = NULL;
raw_prot.sock_array[i] = NULL;
}
tcp_prot.inuse = 0;
tcp_prot.highestinuse = 0;
udp_prot.inuse = 0;
udp_prot.highestinuse = 0;
raw_prot.inuse = 0;
raw_prot.highestinuse = 0;
printk("IP Protocols: ");
//下面进行传输层和网络层之间的衔接:网络层处理函数结果本层的处理后,将查询inet_protos数组,
//匹配合适的传输层协议,调用其对应inet_protocol结构中注册的接收函数
//具体是通过将由inet_protocol_base全局变量指向的inet_protocol结构队列中的元素
//散列到inet_protos哈希表中,从而被网络层使用。该操作通过调用inet_add_protocol函数实现
for(p = inet_protocol_base; p != NULL;)
{
struct inet_protocol *tmp = (struct inet_protocol *) p->next;
inet_add_protocol(p);
printk("%s%s",p->name,tmp?", ":"\n");
p = tmp;
}
/* * Set the ARP module up */
//ARP协议初始化函数
arp_init();
/* * Set the IP module up */
//IP协议初始化函数
ip_init();
}
上面的 inet_add_protocol 函数进行传输层和网络层之间的衔接,将由 inet_protocol_base 指向的链表中 inet_protocol 结构插入到由 inet_protos 表示的数组中,从而被网络层使用。
void inet_add_protocol(struct inet_protocol *prot)
{
unsigned char hash;
struct inet_protocol *p2;
hash = prot->protocol & (MAX_INET_PROTOS - 1);
prot ->next = inet_protos[hash];
inet_protos[hash] = prot;
prot->copy = 0;
/* Set the copy bit if we need to. */
p2 = (struct inet_protocol *) prot->next;
while(p2 != NULL) {
if (p2->protocol == prot->protocol) {
prot->copy = 1;
break;
}
p2 = (struct inet_protocol *) prot->next;
}
}
在 inet_proto_init 函数内部我们还可以看到,该函数最后调用了 arp_init 函数和 ip_init 函数分别进行ARP 协议初始化函数和IP协议初始化函数。
arp_init 函数:
//ARP协议初始化函数
//ARP协议是网络层协议,为了从链路层接收数据包,其必须定义个packet_type结构
void arp_init (void)
{
/* Register the packet type */
arp_packet_type.type=htons(ETH_P_ARP);
//向链路层模块注册
dev_add_pack(&arp_packet_type);
/* Start with the regular checks for expired arp entries. */
//添加定时器
add_timer(&arp_timer);
/* Register for device down reports */
//注册时间通知句柄函数,监听网络设备状态变化事件
register_netdevice_notifier(&arp_dev_notifier);
}
该函数主要完成注册工作,同时注册事件通知句柄函数,监听网络设备状态变化事件,从而对ARP缓存进行及时更新,以维护ARP缓存内容的有效性。
ip_init 函数同样完成该协议接收函数对链路层的注册,同时由于路由表项与网络设备绑定的原因,也必须注册网络设备变化事件监听函数对此类事件进行监听。
/* * IP registers the packet type and then calls the subprotocol initialisers */
//IP初始化函数
void ip_init(void)
{
ip_packet_type.type=htons(ETH_P_IP);
//将IP协议对应的packet_type插入到ptype_base指向的队列中,完成对下层的注册
dev_add_pack(&ip_packet_type);
/* So we flush routes when a device is downed */
//注册网络接收设备时间处理函数
register_netdevice_notifier(&ip_rt_notifier);
/* ip_raw_init(); ip_packet_init(); ip_tcp_init(); ip_udp_init();*/
}
看到没,arp_init 函数和 ip_init 函数内部都调用了 dev_add_pack 函数向链路层模块注册。
将协议对应的packet_type插入到ptype_base指向的队列中,完成对下层的注册
/* * Add a protocol ID to the list. Now that the input handler is * smarter we can dispense with all the messy stuff that used to be * here. */
void dev_add_pack(struct packet_type *pt)
{
if(pt->type==htons(ETH_P_ALL))
dev_nit++;
pt->next = ptype_base;
ptype_base = pt;
}
至此,网络栈初始化工作全部完成,经过 inet_proto_init,arp_init,ip_init 函数的执行,系统完成了由下而上的各层接口之间的衔接。
1> 链路层和网络层经过 ptype_base 指向的 packet_type 结构队列进行衔接,每个packet_type 结构表示一个网络层协议,结构中定义有网络层协议号及其接收函数,链路层模块将根据链路层首部中标识的网络层协议号在队列中进行查找,从而调用对应的接收函数将数据包上传给网络层协议进行处理。
2> 网络层和传输层通过inet_protos散列表进行衔接,表中每个表项指向一个inet_protocol结构队列,每个inet_protocol结构表示一个传输层协议,结构中定义有传输层协议号及其接收函数,网络层模块将根据网络层协议首部中标识的传输层协议号在inet_protos表中进行匹配查询,从而得到传输层协议对应的inet_protocol结构,调用该结构中注册的接收函数,将数据包上传给传输层进行处理。
inet_protocol 结构和 packet_type 结构的作用是类似的,都是作为两层之间的衔接只用,只不过 inet_protocol 结构用于传输层和网络层之间,而 packet_type 结构用于网络层和链路层之间。
综上所述,我们得到网络栈自下而上的数据包传输通道(接收),如下:
在如今Linux最新版本中,虽然网络栈实现代码作了很大的改变,但这个通道基本未变。
ok,前面介绍的倾向于上层协议向下层协议的衔接工作,即注重于数据包接收通道的创建工作,那么数据包发送通道是如何创建的:那就是下层向上层提供发送接口函数供上层直接进行调用。实际上这部分前面博文已经介绍过了(网络栈数据包发送)
这里再简单叙述一下:驱动程序通过hard_start_xmit函数指针向链路层提供发送函数,链路层提供dev_queue_xmit发送函数供网络层调用,而网络层提供 ip_queue_xmit 函数供传输层调用。其中hard_start_xmit函数指针是根据不同的网络设备动态赋值。
另外,这些函数如dev_queue_xmit、ip_queue_xmit 并未采用任何向上层模块注册的方式工作,换句话说,它们都是作为上层模块的已知函数,当上层调用下层发送数据包函数时,直接调用对应函数即可(硬编码的含义)。
当应用层调用write函数开始,我们会先后经历sock_write、inet_write、tcp_write,只有到达tcp_write函数时,才进行数据的真正处理。
以上大致就是系统网络协议栈初始化过程及发送通道和接收通道的建立过程了。
我们最后再捋一捋发送通道和接收通道的区别:
先看下图(显示的目的主机接收到数据包然后到达应用程序的过程,反过来就是从应用程序发送到目的主机物理层网卡设备的过程):
细看这个这个结构,是不是很像一棵倒立的树,如果正立这各结构,这就是成了一棵标准树的结构,每个树节点只有一个父节点,父节点可以有多个子节点。
但这跟网络栈传输数据包有啥关联呢?
ok,我们先看数据包的接收过程(物理层->应用层),从上图,以及结合前面的接收通道分析,由下往上,相当于一棵树从根节点出发到某个叶子节点,中间的某个节点可能有多个子节点,我们就得选择其中一个节点作为途径向下走,同理,协议栈也是这样。
看图,进入的帧经过以太网驱动程序,出现了三条通道,此时就需要证据帧首部中的类型选择走哪个通道(如IP),到了IP层,又出现了四条通道,此时又得根据IP首部中的传输层协议号进行选择,到了传输层就可通过端口号定位到对应应用程序了。所以由下往上有一个根据对应类型进行选择通道的过程;
反过来,数据包的发送则不需要这么麻烦了,因为任意一个子节点都只会有一个父节点,应用程序发送的数据就已经知道是属于哪个协议了,直接进入对应协议的传输层,然后层层往下,不需要一个通道选择的过程了。
较为牵强的白话表述,就是一棵树,从根节点到叶子节点有多条路径,但从叶子节点到根节点是唯一路径。
参考资料《Linux内核网络栈源代码情景分析》