三、进阶 | Linux 内核概念(4):内核的通知链
Linux内核中的通知链
引言
Linux内核是一个庞大的C语言代码块,由许多不同的子系统组成。每个子系统都有自己的目的,与其他子系统无关。但通常一个子系统想要知道其他子系统的信息。Linux内核中有一个特殊的机制可以部分解决这个问题。这个机制的名称是 - 通知链
,其主要目的是为不同的子系统提供一个订阅其他子系统异步事件的方式。请注意,这种机制仅用于内核内部的通信,还有其他机制用于内核与用户空间之间的通信。
在我们考虑通知链
API和这个API的实现之前,让我们从理论角度看看通知链
机制,就像我们在这本书的其他部分所做的那样。与通知链
机制相关的一切都位于include/linux/notifier.h头文件和kernel/notifier.c源代码文件中。那么让我们打开它们,开始深入了解。
与通知链相关的数据结构
让我们从相关的数据结构开始考虑通知链
机制。正如我上面所写的,主要的数据结构应该位于include/linux/notifier.h头文件中,所以Linux内核提供了一个通用的API,不依赖于特定的架构。总的来说,通知链
机制表示一个列表(这就是为什么它被称为链
)的回调函数,当事件发生时将被执行。
所有这些回调函数在Linux内核中都表示为notifier_fn_t
类型:
typedef int (*notifier_fn_t)(struct notifier_block *nb, unsigned long action, void *data);
所以我们可以看到它接受以下三个参数:
nb
- 函数指针的链表(现在将看到);action
- 事件的类型。一个通知链可能支持多个事件,所以我们需要这个参数来区分一个事件和其他事件;data
- 用于存储私有信息。实际上它允许提供有关事件的额外数据信息。
此外,我们可以看到notifier_fn_t
返回一个整数值。这个整数值可能是以下之一:
NOTIFY_DONE
- 订阅者对通知不感兴趣;NOTIFY_OK
- 通知被正确处理;NOTIFY_BAD
- 出了点问题;NOTIFY_STOP
- 通知完成,但不应再为这个事件调用更多的回调。
所有这些结果在include/linux/notifier.h头文件中定义为宏:
#define NOTIFY_DONE 0x0000
#define NOTIFY_OK 0x0001
#define NOTIFY_BAD (NOTIFY_STOP_MASK|0x0002)
#define NOTIFY_STOP (NOTIFY_OK|NOTIFY_STOP_MASK)
其中NOTIFY_STOP_MASK
由以下表示:
#define NOTIFY_STOP_MASK 0x8000
宏定义,意味着在下次通知期间不会调用回调。
Linux内核的每个部分,如果希望在某个事件上得到通知,都应该提供自己的notifier_fn_t
回调函数。通知链
机制的主要作用是在异步事件发生时调用某些回调。
通知链
机制的主要构建块是notifier_block
结构:
struct notifier_block {
notifier_fn_t notifier_call;
struct notifier_block __rcu *next;
int priority;
};
它在include/linux/notifier.h文件中定义。这个结构包含指向回调函数的指针 - notifier_call
,链接到下一个通知回调和回调函数的priority
,因为具有较高优先级的函数将首先执行。
Linux内核提供了以下四种类型的通知链:
- 阻塞通知链;
- SRCU通知链;
- 原子通知链;
- 原始通知链。
让我们按顺序考虑所有这些类型的通知链:
在第一种情况中,对于阻塞通知链
,回调将在进程上下文中被调用/执行。这意味着通知链中的调用可能会被阻塞。
第二种SRCU通知链
代表了阻塞通知链
的替代形式。在第一种情况下,阻塞通知链使用rw_semaphore
同步原语来保护链链接。SRCU
通知链也在进程上下文中运行,但使用了一种特殊的RCU机制,允许在读取关键部分中阻塞。
第三种情况对于原子通知链
在中断或原子上下文中运行,并由自旋锁同步原语保护。最后的原始通知链
提供了一种特殊类型的没有对回调进行任何锁定限制的通知链。这意味着保护的责任在于调用方。当我们想要用非常特定的锁定机制来保护我们的链时,这非常有用。
如果我们查看notifier_block
结构的实现,我们会看到它包含指向通知链列表中下一个元素的指针,但我们没有头部。实际上,这样的列表头部在依赖于通知链类型的单独结构中。例如对于阻塞通知链
:
struct blocking_notifier_head {
struct rw_semaphore rwsem;
struct notifier_block __rcu *head;
};
或对于原子通知链
:
struct atomic_notifier_head {
spinlock_t lock;
struct notifier_block __rcu *head;
};
现在我们已经对通知链
机制有所了解,让我们考虑其API的实现。
通知链
通常在发布/订阅机制中有两个方面。一个方面是想要获得通知的一方,另一个方面是生成这些通知的一方。我们将从两个方面考虑通知链机制。在这部分,我们将考虑阻塞通知链
,因为其他类型的通知链与它类似,主要区别在于保护机制。
在通知生产者能够产生通知之前,首先应该初始化通知链的头部。例如让我们考虑与内核可加载模块相关的通知链。如果我们查看kernel/module.c源代码文件,我们会看到以下定义:
static BLOCKING_NOTIFIER_HEAD(module_notify_list);
它定义了可加载模块阻塞通知链的头部。BLOCKING_NOTIFIER_HEAD
宏在include/linux/notifier.h头文件中定义,并展开为以下代码:
#define BLOCKING_INIT_NOTIFIER_HEAD(name) do { \
init_rwsem(&(name)->rwsem); \
(name)->head = NULL; \
} while (0)
所以我们可以看到它接受一个阻塞通知链头部的名称,并初始化读写信号量并将头部设置为NULL
。除了BLOCKING_INIT_NOTIFIER_HEAD
宏,Linux内核还提供了ATOMIC_INIT_NOTIFIER_HEAD
、RAW_INIT_NOTIFIER_HEAD
宏和srcu_init_notifier
函数,用于初始化原子和其他类型的通知链。
在初始化通知链的头部之后,想要从给定通知链接收通知的子系统应该使用取决于类型的特定函数进行注册。如果你查看include/linux/notifier.h头文件,你会看到以下四个函数用于此:
extern int atomic_notifier_chain_register(struct atomic_notifier_head *nh,
struct notifier_block *nb);
extern int blocking_notifier_chain_register(struct blocking_notifier_head *nh,
struct notifier_block *nb);
extern int raw_notifier_chain_register(struct raw_notifier_head *nh,
struct notifier_block *nb);
extern int srcu_notifier_chain_register(struct srcu_notifier_head *nh,
struct notifier_block *nb);
正如我上面所写的,我们将只覆盖这部分的阻塞通知链,所以让我们考虑blocking_notifier_chain_register
函数的实现。这个函数的实现位于kernel/notifier.c源代码文件中,我们可以看到blocking_notifier_chain_register
接受两个参数:
nh
- 通知链的头部;nb
- 通知描述符。
现在让我们看看blocking_notifier_chain_register
函数的实现:
int raw_notifier_chain_register(struct raw_notifier_head *nh,
struct notifier_block *n)
{
return notifier_chain_register(&nh->head, n);
}
正如我们所看到的,它只是返回了同一个源代码文件中的notifier_chain_register
函数的结果,正如我们所理解的,这个函数为我们完成了所有工作。notifier_chain_register
函数的定义如下:
int blocking_notifier_chain_register(struct blocking_notifier_head *nh,
struct notifier_block *n)
{
int ret;
if (unlikely(system_state == SYSTEM_BOOTING))
return notifier_chain_register(&nh->head, n);
down_write(&nh->rwsem);
ret = notifier_chain_register(&nh->head, n);
up_write(&nh->
nh->rwsem);
return ret;
}
正如我们所看到的,blocking_notifier_chain_register
的实现相当简单。首先有一个检查,检查当前系统状态,如果系统处于启动状态,我们只需调用notifier_chain_register
。否则我们做同样的调用notifier_chain_register
,但正如你所见,这个调用受到读写信号量的保护。现在让我们看看notifier_chain_register
函数的实现:
static int notifier_chain_register(struct notifier_block **nl,
struct notifier_block *n)
{
while ((*nl) != NULL) {
if (n->priority > (*nl)->priority)
break;
nl = &((*nl)->next);
}
n->next = *nl;
rcu_assign_pointer(*nl, n);
return 0;
}
这个函数只是将新的notifier_block
(由想要接收通知的子系统提供)插入到通知链列表中。除了订阅事件,订阅者还可以使用一组unsubscribe
函数从某些事件中取消订阅:
extern int atomic_notifier_chain_unregister(struct atomic_notifier_head *nh,
struct notifier_block *nb);
extern int blocking_notifier_chain_unregister(struct blocking_notifier_head *nh,
struct notifier_block *nb);
extern int raw_notifier_chain_unregister(struct raw_notifier_head *nh,
struct notifier_block *nb);
extern int srcu_notifier_chain_unregister(struct srcu_notifier_head *nh,
struct notifier_block *nb);
当通知的生产者想要通知订阅者有关事件时,将调用*.notifier_call_chain
函数。正如你可能已经猜到的,每种类型的通知链都提供了自己的函数来产生通知:
extern int atomic_notifier_call_chain(struct atomic_notifier_head *nh,
unsigned long val, void *v);
extern int blocking_notifier_call_chain(struct blocking_notifier_head *nh,
unsigned long val, void *v);
extern int raw_notifier_call_chain(struct raw_notifier_head *nh,
unsigned long val, void *v);
extern int srcu_notifier_call_chain(struct srcu_notifier_head *nh,
unsigned long val, void *v);
让我们考虑blocking_notifier_call_chain
函数的实现。这个函数定义在kernel/notifier.c源代码文件中:
int blocking_notifier_call_chain(struct blocking_notifier_head *nh,
unsigned long val, void *v)
{
return __blocking_notifier_call_chain(nh, val, v, -1, NULL);
}
正如我们所看到的,它只是返回了__blocking_notifier_call_chain
函数的结果。正如我们所看到的,blocking_notifer_call_chain
接受三个参数:
nh
- 通知链列表的头部;val
- 通知的类型;v
- 输入参数,可能被处理程序使用。
但__blocking_notifier_call_chain
函数接受五个参数:
int __blocking_notifier_call_chain(struct blocking_notifier_head *nh,
unsigned long val, void *v,
int nr_to_call, int *nr_calls)
{
...
...
...
}
其中nr_to_call
和nr_calls
是要调用的通知函数的数量和发送的通知数量。正如你可能猜到的,__blocking_notifer_call_chain
函数和其他通知类型函数的主要目标是在事件发生时调用回调函数。__blocking_notifier_call_chain
的实现相当简单,它只是调用了同一个源代码文件中的notifier_call_chain
函数,并受到读写信号量的保护:
int __blocking_notifier_call_chain(struct blocking_notifier_head *nh,
unsigned long val, void *v,
int nr_to_call, int *nr_calls)
{
int ret = NOTIFY_DONE;
if (rcu_access_pointer(nh->head)) {
down_read(&nh->rwsem);
ret = notifier_call_chain(&nh->head, val, v, nr_to_call,
nr_calls);
up_read(&nh->rwsem);
}
return ret;
}
并返回其结果。在这种情况下,所有工作都由notifier_call_chain
函数完成。这个函数的主要目的是在异步事件发生时通知注册的通知者:
static int notifier_call_chain(struct notifier_block **nl,
unsigned long val, void *v,
int nr_to_call, int *nr_calls)
{
...
...
...
ret = nb->notifier_call(nb, val, v);
...
...
...
return ret;
}
就是这样。总的来说,看起来相当简单。
现在让我们考虑一个与可加载模块相关的简单示例。如果我们查看kernel/module.c。正如我们在这部分已经看到的,有:
static BLOCKING_NOTIFIER_HEAD(module_notify_list);
在kernel/module.c源代码文件中定义了module_notify_list
。这个定义确定了与内核模块相关的通知链列表的头部。至少有三个以下事件:
- MODULE_STATE_LIVE
- MODULE_STATE_COMING
- MODULE_STATE_GOING
Linux内核的某些子系统可能对这些事件感兴趣。例如跟踪内核模块的状态。大多数通知链不是直接调用atomic_notifier_chain_register
、blocking_notifier_chain_register
等,而是提供了一组用于注册的包装器。这些模块事件的注册是通过这样的包装器进行的:
int register_module_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_register(&module_notify_list, nb);
}
如果我们查看kernel/tracepoint.c源代码文件,我们会看到在初始化tracepoints时有这样的注册:
static __init int init_tracepoints(void)
{
int ret;
ret = register_module_notifier(&tracepoint_module_nb);
if (ret)
pr_warn("Failed to register tracepoint module enter notifier\n");
return ret;
}
其中tracepoint_module_nb
提供了回调函数:
static struct notifier_block tracepoint_module_nb = {
.notifier_call = tracepoint_module_notify,
.priority = 0,
};
当MODULE_STATE_LIVE
、MODULE_STATE_COMING
或MODULE_STATE_GOING
中的一个事件发生时。例如,MODULE_STATE_LIVE
或MODULE_STATE_COMING
通知将在执行init_module系统调用期间发送。或者例如MODULE_STATE_GOING
将在执行delete_module系统调用
期间发送:
SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
unsigned int, flags)
{
...
...
...
blocking_notifier_call_chain(&module_notify_list,
MODULE_STATE_GOING, mod);
...
...
...
}
因此,当从用户空间调用这些系统调用之一时,Linux内核将根据系统调用发送某些通知,并且tracepoint_module_notify
回调函数将被调用。
就是这样。
链接
"《Linux嵌入式必考必会》专栏,专为嵌入式开发者量身打造,聚焦Linux环境下嵌入式系统面试高频考点。涵盖基础架构、内核原理、驱动开发、系统优化等核心技能,实战案例与理论解析并重。助你快速掌握面试关键,提升竞争力。