深入理解Linux网络技术内 幕(四)——通知链

文章目录

  • 前言
  • 使用通知链的原因
  • 概论
  • 定义链
  • 链注册
  • 链上的通知事件
  • 网络子系统的通知链
    • 包裹函数
    • 范例
  • 通过/proc文件系统调整
  • 涉及的函数和变量
  • 涉及的文件和目录


前言

内核的很多子系统之间具有很强的相互依赖性,因此,其中一个子系统侦测到的或者产生的事件,其他子系统可能都有兴趣。为了实现这种交互需求,Linux使用了所谓的通知链(notification chain)
这篇博客你将看到:

  • 通知链如何声明以及网络代码定义了哪些链(chain)
  • 内核子系统如何向通知链注册
  • 内核子系统如何在链上产生通知消息。
    注意,通知链只在内核子系统之间使用。内核和用户空间之间的通知信息则是依赖其他机制,如上一篇博客介绍(虚拟文件系统、系统调用等等)

使用通知链的原因

假设我们有如下所示带有4个接口的Linux路由器。该图显示了路由器与五个网络之间的关系,还有一份简化的路由表。
深入理解Linux网络技术内 幕(四)——通知链_第1张图片网络A直接连接至RT的接口eth0,而网络F没有直接连接至RT,但是RT的eth3则直接连接至另一个路由器,其接口为IP1,并且第二个路预期知道该如何联系网络F。其他的案例都很类似。简而言之,有些网路是直接连接的,而其他网络则需要一个或多个附加的路由器协助才能联系到。
有关路由代码如何处理这种情况的详细描述,后面分析。今天主要的介绍的是通知链角色。假设接口eth3由于网络断线使得管理命令(如:ifconfgi eth3 down)失效或者造成硬件失败,结果RT无法联系网络D、E和F(以及A、B、C网络中依赖RT联机的系统),因此应该从路由表中删除。谁将通知路由子系统该接口的失效?那就是通知链。

分析下面这个稍微复杂的案例,路由子系统会与动态路由协议交互,这种协议可以调整一个或多个路由表,使其吻合网络拓扑结构,因此当拓扑结构许可时就可以处理接口失败的问题(也就是有冗余路径存在)。
深入理解Linux网络技术内 幕(四)——通知链_第2张图片

上图中,RT通过网络A和网路E而联系网络F。最初会选择R是因为其成本较少,但现在E无法联系,因此,路由表应该更新网络F的路由改走网络A。这种决策的基础可能包括一些本地主机事件,诸如设备注册和删除,以及路由配置中的复杂因素和所用的路由协议。在任何情况下,管理路由表的路由子系统必须从其他子系统哪里收到相关的信息通知,因此产生了通知链的需求。

概论

通知链就是一份简单的函数列表,当给定事件发生时予以执行,每个函数都让另一个子系统知道,调用此函数的子系统内所发生的的一个事件或者子系统所侦测到的一个事件。
因此,就每条通知链而言,都有被动端(被通知者)和主动端(通知者),也就是所谓的发布-订阅(publish-and-subscribe)模型

  • 被通知者(notified)就是要求接收某事件的子系统,而且会提供回调函数予以调用。
  • 通知者(notifier)就是感受到一个事件并调用回呼函数的子系统
    所执行的函数是由被通知者的系统所选取,绝不是链条的拥有者(产生通知信息的子系统)决定该执行什么函数。拥有者只是定义这份列表而已;任何内核子系统都可以对该链条注册一个回调函数以接收通知信息。

通知链的使用使源码更易于编写和维护。想象一下,一个通用函数不使用通知链,该如何把一个事件通知给外部子系统:

if(subsystem_X_enabled)
{
	do_something_1;	
}
if(subsystem_Y_enabled)
{
	do_something_2;
}
if(subsystem_Z_enabled)
{
	do_something_3;
}

换言之,必须为每个可能对一个事件感兴趣的子系统都引入一个条件子句,结果每次某人添加一个子系统给内核时,此子系统的维护者就得添加一个子句。
子系统维护者不可能去追踪每个被添加到内核的子系统。然而,每位子系统维护者应该知道:

  • 自己对来自其他子系统的那种事件感兴趣。
  • 自己知道的事件有哪几种,并且其他子系统可能感兴趣的事件又是那种。
    因此,通知链允许每个子系统和其他子系统共享发生的事件,而无需知道就是是那些子系统产生事件,以及那些子系统为什么感兴趣。

定义链

通知链列表元素的类型是notifier_block,其定义如下:

struct notifier_block
{
	int (*notifier_call)(struct notifirt_block *self, unsigned long, void *);
	struct notifier_block *next;
	int priority;
};

notifier_call是要执行的函数,next是用于链接列表的元素,而priority代表的是该函数的优先级。较高优先级的函数会被先执行。但是实际中,注册时都不会理会notifier_block中定义的priority,意味着获得其默认值0.因此,执行的次序仅依赖于注册次序(也就是半随机次序)。notifier_call的返回值分析见下面的链上的通知事件一节
notifier_block实例的常见名称有xxx_chain、xxx_notifier_chain以及xxx_notifier_list。

链注册

当一个内核组件对给定通知链的事件感兴趣时,可以用通用函数notifier_chain_register予以注册。内核也提供一组内涵notifier_chain_register的包裹函数,如下列出了主要的API以及相关的包裹函数,可用于向三条链inetaddr_chain、inet6addr_chain以及netdev_chain注册及删除。
深入理解Linux网络技术内 幕(四)——通知链_第3张图片深入理解Linux网络技术内 幕(四)——通知链_第4张图片

对于每条链条而言,那些notifier_block实体被插入到一个按优先级排序的列表中。相同优先级的元素则按插入时间排序:新的元素排在尾端。
对通知链的访问受到notifier_lock锁的保护。所有通知链只用一个锁并不是什么大不了的限制,而且也不影响性能,因此子系统通常只是在引导或模块加载时注册其notifier_call函数,然后从那时起对列表的访问都是只读的(即共享)。

因为调用notifer_chain_register函数就是把回调函数插入到所有列表中,因此必须把列表指定为输入参数。然而,此函数很少被直接调用,通常是改用通用的包裹函数。

int notifier_chain_register(struct notifer_block **list, struct notifier_block *n)
{
	write_lock(&notifier_lock);
	while(*list)
	{
		if(n->priority > (*list)->priority)
			break;
		list = &((*list)->next);
	}
	n->next = *list;
	*list = n;
	write_unlock(&notifier_lock);
	return 0;
}

链上的通知事件

通知信息由定义于kernelsys.c中的notifier_call_chain产生,而此函数只是按优先级次序调用对此链注册的所有回调函数。注意,回调函数是在调用notifier_call_chian的进程上下文(context)中执行的。然而,回调函数也是可以实现成把通知信息排进某处的队列,然后唤醒查看此通知信息的进程。

int notifier_call_chain(struct notifier_block **n, unsigned long val, void *v)
{
	int ret = NOTIFY_DONE;
	struct notifier_block *nb = *n;
	while(*nb)
	{
		ret = nb->notifier_call(nb, vall, v);
		if(ret  & NOTIFY_STOP_MASK)
		{
				return ret
		}
		nb = nb->next;
  }   
  return ret;
}

以下是其三个输入参数的意义:

  • n:通知链
  • val:事件类型。链本身标识的一组事件;val明确标识一种事件类型(也就是NETDEV_REGISTER)。
  • v:此输入参数可由各种各样的客户所注册的处理函数使用,在不同情况下可以有不同用途。例如,当一个新的网络设备在内核注册时,相关的通知信息会使用v以标识net_device数据结构。

notifier_call_chain所调用的回调函数可以返回定义在include/linux/notifier.h中的任何NOTIFY_XXX的值:

  • NOTIFY_OK:通知信息被正确处理了。
  • NOTIFY_DONE:对通知信息不感兴趣
  • NOTIFY_BAD:有些事情出错。停止调用此事件的回调函数。
  • NOTIFY_STOP:函数被正确调用。然而,此事件不需要进一步调用其他函数。
  • NOTIFY_STOP_MASK:此标识由notifier_call_chain检查,以了解是否停止调用回调函数,或者继续调用下去。NOTIFY_BAD和NOTIFY_STOP在其定义中包括了此标识。
    notifier_call_chain捕获并返回由最后一个调用的回调函数所接收的返回值。无论是否所有回调函数都已被调用,或者其中之一由于NOTIFY_BAD或NOTIFY_STOP的返回值中断了循环,都是如此。
    注意,在同一时间不同CPU上的相同的通知链有可能同时调用notifier_call_chain。回调函数的责任是在必要的地方处理互斥和串行化。

网络子系统的通知链

内核定义了至少10种不同的通知链。我们感兴趣的是那些对网络代码表征特别重要事件的通知链。主要是:

  • inetaddr_chain

发送有关本地接口上的IPv4地址的插入、删除以及变更的通知信息。在后面的博客会描述这类通知信息何时产生。IPv6使用类似的链(ipnet6addr_chain)

  • inetdev_chain

发送有关网络设备注册状态的通知信息。在后面会描述这类通知信息何时产生。

对于网络子系统所用的这些链以及其他链而言,其目的和使用都会在相关的关于通知者子系统中描述。
网络代码也可以注册其他内核组件产生的通知信息。例如,某些NIC设备驱动程序可以用reboot_notifier_list链注册;当系统重新引导时,此链会发出警告。

包裹函数

大多数通知链都有一组包裹函数,可以用于注册和删除。例如,下列包裹函数可用于向netdev_chain注册:

int register_netdev_notifier(struct notifier_block *nb)
{
	return notifier_chain_register(&netdev_chain,nb);
}

包裹函数常见名称包括[un]register_xxx_notifier、xxx_[un]register_notifier以及xxx_[un]register。

范例

通知链注册通常发生在感兴趣的内核组件初始化时。例如,下面是从net/ipv4/fib_fronted.c中取出的片段;ip_fib_init就是路由代码所使用的初始化函数。

static struct notifier_block fib_inetaddr_notifier = {
	.notifier_call = fib_inetaddre_event,
};
static struct notifier_block fib_netdev_notifier = {
	.notifier_call = fib_netdev_event,
};
void __init ip_fib_init(void)
{
	........
	register_netdevice_notifier(&fib_netdev_notifier);
	register_netaddr_notifier(&fib_inetaddr_notifier);
}

改变本地IP地址配置以及改变本地设备的注册状态,都会影响路由表。

通过/proc文件系统调整

就这篇博客而言,与/proc中的文件无关。

涉及的函数和变量

名称 描述
函数和宏
notifier_chain_register +包裹函数 前两个函数是为了通知链注册和删除回调处理
notifier_chain_unregister + 包裹函数
notifier_call_chain 送出特定种类事件的所有通知信息
数据结构
struct notifier_block 定义通知信息的处理函数,包括要调用的回调函数

涉及的文件和目录

深入理解Linux网络技术内 幕(四)——通知链_第5张图片

你可能感兴趣的:(Linux网络,linux,网络,运维)