前言

内核中有许多子系统,他们相互独立,但又具有很强的依赖性。因此其中一个子系统侦测到的或者产生的事件其他子系统可能都有兴趣,那么为了实现这样的交互需求,Linux使用了所谓的通知链(notification chain)。


本博客包含的主要内容

1.通知链如何声明以及内核代码定义了那些链(chain)。

2.内核子系统如何向通知链注册。

内核子系统如何在链上产生通知消息。


正文


为什么需要通知链?

考虑下面一个场景或者例子。假设你的所在的网络D有一台路由器RT可以直接访问网络B和网络C,又可以通过网络B或者网络C访问到网络A。这样的一个拓扑结构。最初通过网络C这条路径访问网络A是因为这条路径相对来说成本比较少,但是现在网络C这条路径坏了无法通过这条路径访问A了因此路由表应该更新网络A路由改走网络B,这种决策基础包括一些本地主机事件,例如设备的删除和注册,以及路由配置中的复杂因素和所用路由协议,在任何情况下路由表的路由子系统必须从其他子系统那里收到相关信息的通知,因而产生了通知链的需求。


通知链

通知链本质上是一份简单的函数列表,简单理解就是一系列执行不同事件的函数,每个函数都让另外一个子系统知道,调用这个函数的这个系统发生了什么事件。也就意味着通知链有被动端(被通知者)和主动端(通知者),也就是所谓的发布-订阅模型。


被通知者:就是要接受某个事件通知的子系统,他来提供call函数。

通知者:就是将事件传给被通知者,他调用call函数来通知。


这两种角色是相对的,其实每个子系统既是通知者也是被通知者。子系统自己应该明确对其他子系统那些事件感兴趣,并且自己知道的事件有哪几种,其他子系统可能感兴趣的又有哪些。

在内核中的设计其实包括三个部分,第一个是定义了一个链,只不过这个链条存的是一个一个的call函数。第二是想着个链条添加函数。第三就是执行这个链条中的每个call函数。


定义链

先给出内核代码:include/linux/notifier.h

struct notifier_block;

typedef	int (*notifier_fn_t)(struct notifier_block *nb,
			unsigned long action, void *data);

struct notifier_block {
	notifier_fn_t notifier_call;
	struct notifier_block __rcu *next;
	int priority;
};

链列表元素的类型为 notifier_block 其中:

notifier_call是要执行的函数

next用于指向链表中的下一个元素

priority代表的是该函数的优先级,较高的优先级会被优先执行。但是在实际中,注册时几乎都不管这个priority,执行的次序仅仅依赖于注册的次序。

定义这个链的目的可以理解成定义了一连串的连续执行的函数执行完一个执行下一个。


链注册

还是先给出代码:kernel/notifier.c

//注册
static int notifier_chain_register(struct notifier_block **nl,
				   struct notifier_block *n,
				   bool unique_priority)
{
	while ((*nl) != NULL) {
		if (unlikely((*nl) == n)) {
			WARN(1, "notifier callback %ps already registered",
			     n->notifier_call);
			return -EEXIST;
		}
		if (n->priority > (*nl)->priority)
			break;
		if (n->priority == (*nl)->priority && unique_priority)
			return -EBUSY;
		nl = &((*nl)->next);
	}
	n->next = *nl;
	rcu_assign_pointer(*nl, n);
	trace_notifier_register((void *)n->notifier_call);
	return 0;
}
//注销
static int notifier_chain_unregister(struct notifier_block **nl,
		struct notifier_block *n)
{
	while ((*nl) != NULL) {
		if ((*nl) == n) {
			rcu_assign_pointer(*nl, n->next);
			trace_notifier_unregister((void *)n->notifier_call);
			return 0;
		}
		nl = &((*nl)->next);
	}
	return -ENOENT;
}

注册的目的是:当一个内核组建对给定通知链条的事件感兴趣时,可以用这个

int notifier_chain_register()

予以注册,当然这个函数很少被直接调用,都是调用在内核中提供的一些内含notifier_chain_register的包裹函数,例如跟网络有关的

可用于向inetaddr_chain、inet6addr_chain、netdev_chain注册的

register_inetaddr_notifier、register_inet6addr_notifier、register_netdevice_notifier。

给一个例子:

//如下函数可用于向netdev_chain注册
int register_netdevice_notifier(struct notifier_Block *nb){
					return notifier_chain_register(&netdev_chain,nb);
}

根据代码不难看出,对于每条链条,notifier_block实体被插入到一个按优先级排序的列表,相同优先级元素按照时间顺序插入,新的元素在最底下。

//如果n的优先级比*nl的优先级高那么循环结束
if (n->priority > (*nl)->priority)
			break;
//否则*nl继续向后移 继续进入循环比较
nl = &((*nl)->next);

//如果n的优先级比*nl大了就讲n插入到*nl的前面
n->next = *nl;
rcu_assign_pointer(*nl, n);//根据别的博客看到的讲解这个函数等价于*nl=n

注销的话就相对简单

//循环判断找到了要注销的然后执行注销
//从链表中移除。
if ((*nl) == n) {
			rcu_assign_pointer(*nl, n->next);
			trace_notifier_unregister((void *)n->notifier_call);
			return 0;
		}
		nl = &((*nl)->next);

链上的通知事件

还是先给出内核的代码:kernel/notifier.c


static int notifier_call_chain(struct notifier_block **nl,
			       unsigned long val, void *v,
			       int nr_to_call, int *nr_calls)
{
	int ret = NOTIFY_DONE;
	struct notifier_block *nb, *next_nb;

	nb = rcu_dereference_raw(*nl);

	while (nb && nr_to_call) {
		next_nb = rcu_dereference_raw(nb->next);

#ifdef CONFIG_DEBUG_NOTIFIERS
		if (unlikely(!func_ptr_is_kernel_text(nb->notifier_call)))
    {
			WARN(1, "Invalid notifier called!");
			nb = next_nb;
			continue;
		}
#endif
		trace_notifier_run((void *)nb->notifier_call);
		ret = nb->notifier_call(nb, val, v);

		if (nr_calls)
			(*nr_calls)++;

		if (ret & NOTIFY_STOP_MASK)
			break;
		nb = next_nb;
		nr_to_call--;
	}
	return ret;
}

这个函数的主要功能是按照优先级次序调用对此链注册的所有回调函数。值得注意的是,回调函数是在调用notifier_call_chain的进程上下文(context)中执行的。

代码中的核心是:

	int ret = NOTIFY_DONE;//返回值指调用回调函数之后的结果。
	struct notifier_block *nb, *next_nb; //执行的链条

	while (nb && nr_to_call) {
    	ret = nb->notifier_call(nb, val, v);//执行过后返回一个执行的结果
    
		if (nr_calls)
			(*nr_calls)++;

		if (ret & NOTIFY_STOP_MASK)
            //这里判定结果如果成功了这个ret & NOTIFY_STOP_MASK值为假不会跳出循环
            //否则跳出循环
			break;
		nb = next_nb;//继续执行下一个
    }

参数的含义:

nl

指的通知链

val

事件类型。链本身标识的一组事件,val明确标识一种事件类型

v

此参数可由各种各样的客户所注册的处理函数使用,在不同情况下可以有不同用途。例如当一个新的网络设备在内核注册时,相关通知信息会使用v以标识net_device数据结构。

nr_to_call 

要调用的通知程序函数数量。如果不需要将此参数的值为 -1。

nr_calls 

记录发送的通知数。不需要的话将此字段的值为 NULL。

ret

代表的返回值也在include/linux/notifier.h给出了定义

#define NOTIFY_DONE		0x0000		/* Don't care */
#define NOTIFY_OK		0x0001		/* Suits me */
#define NOTIFY_STOP_MASK	0x8000		/* Don't call further */
#define NOTIFY_BAD		(NOTIFY_STOP_MASK|0x0002)
						/* Bad/Veto action */
/*
 * Clean way to return from the notifier and stop further calls.
 */
#define NOTIFY_STOP		(NOTIFY_OK|NOTIFY_STOP_MASK)

每调用一个函数都要有个返回值来表示执行结果,内核代码中定义了

NOTIFY_DONE:对通知信息不敢兴趣

NOTIFY_OK:通知信息被正确处理

NOTIFY_STOP_MASK:此标识由notifier_call_chain检查,以了解是否停止调用回调函数,或者继续调用。NOTIFY_BAD、NOTIFY_STOP都在其定义中包括了此标识。

NOTIFY_BAD: 有些事情出错。停止调用此事件的回调函数

NOTIFY_STOP:函数被正确调用,然而此事件不需要进一步调用其他回调函数。

    int ret0=NOTIFY_DONE	;
    int ret1=NOTIFY_OK;
    int ret2=NOTIFY_STOP_MASK;
    int ret3=NOTIFY_BAD;
    int ret4=NOTIFY_STOP;

    printf("%x\n",ret0 &NOTIFY_STOP_MASK);//0
    printf("%x\n",ret1 &NOTIFY_STOP_MASK);//0
    printf("%x\n",ret2 &NOTIFY_STOP_MASK);//8000
    printf("%x\n",ret3 &NOTIFY_STOP_MASK);//8000
    printf("%x\n",ret4 &NOTIFY_STOP_MASK);//8000

我手动模拟将

ret & NOTIFY_STOP_MASK 

的值表示出来,如果成功执行这个表达式为假,没有成功执行表达式为真,就会跳出循环。不再执行此链条的回调函数。


那么以上就是对通知链的学习和分享。有问题欢迎大家评论区一起讨论。