[TOC]

kernel/notifier.c 内核通知链(Kernel Notifier Chains) 实现内核子系统间的解耦通信

历史与背景

这项技术是为了解决什么特定问题而诞生的?

内核通知链(Notifier Chains)机制的诞生,是为了解决在 Linux 这样一个庞大而复杂的单体内核(Monolithic Kernel)中,不同子系统之间低耦合的异步通信问题。

在内核中,一个子系统发生的事件(如网络设备状态变化、CPU上线/下线、模块加载/卸载)往往需要通知其他多个、在编译时甚至不知道存在的子系统。 如果采用直接函数调用的方式,将会导致子系统之间产生紧密的耦合关系,使得代码难以维护和扩展。例如,网络设备子系统在网卡注册时不应硬编码去调用IP层、桥接层等所有可能关心此事件的模块代码。

为了避免这种情况,内核引入了通知链机制,它采用了一种**发布-订阅(Publish-Subscribe)**的设计模式。

  • 发布者(Publisher):事件发生的子系统,它只管向一个“事件通道”(即通知链的头部)发布通知。
  • 订阅者(Subscriber):关心该事件的其他子系统,它们将自己的处理函数(回调函数)注册到这个“事件通道”中。

通过这种方式,发布者和订阅者实现了解耦,发布者无需知道有哪些订阅者,以及订阅者会如何处理事件。这极大地提高了内核代码的模块化程度和灵活性。

它的发展经历了哪些重要的里程碑或版本迭代?

通知链机制是内核中一个基础且演进多年的特性。

  • 基本实现:最初的通知链是一个简单的、按优先级排序的函数指针链表。 核心数据结构是 struct notifier_block,它包含一个回调函数指针、一个指向下一个节点的指针和一个优先级字段。
  • 引入多种类型:为了适应不同的使用场景,特别是并发和上下文(进程上下文 vs 中断上下文)的要求,通知链逐渐分化为多种类型。最主要的区分是**阻塞型(Blocking)原子型(Atomic)**通知链。
  • 锁机制与RCU的引入:为了确保在多核环境下的线程安全,通知链的实现不断完善其锁机制。阻塞型通知链使用读写信号量(rwsem)来保护链表,允许回调函数阻塞。 原子型通知链则为了在中断等原子上下文中安全使用,采用了自旋锁(spinlock)进行注册/注销保护,并在调用时结合RCU(Read-Copy Update)机制,使得通知的发送过程无需加锁,性能极高。
  • 引入SRCU和Raw类型:后续又增加了**SRCU(Sleepable Read-Copy Update)**通知链,它是一种可以在进程上下文中运行,但回调函数可以睡眠的RCU变体。 **原始(Raw)**通知链则不提供任何内部锁机制,将所有同步的责任交由调用者,提供了最大的灵活性。

目前该技术的社区活跃度和主流应用情况如何?

通知链是Linux内核中一项非常核心、稳定且被广泛使用的基础IPC(内部过程通信)机制。

  • 主流应用:几乎所有需要向其他模块广播状态变化的内核子系统都在使用通知链。常见的例子包括:
    • 网络设备事件 (netdev_chain):通知网络设备的注册、注销、状态变更等。
    • CPU热插拔事件 (cpu_chain):通知CPU的上线和下线。
    • 模块加载/卸载事件 (module_notify_chain):通知内核模块的变化。
    • 系统重启/关机事件 (reboot_notifier_list):允许子系统在系统重启前执行清理工作。
    • 内存热插拔事件 (memory_chain)。

核心原理与设计

它的核心工作原理是什么?

通知链的核心是一个由 struct notifier_block 结构组成的链表。

  1. 数据结构

    • struct notifier_block:每个订阅者创建一个该类型的实例,其中最重要的成员是 notifier_call,一个指向回调函数的指针。其他成员包括 next 指针用于链接下一个节点,以及 priority 用于决定回调函数的执行顺序(值越大优先级越高)。
    • 通知链头(Notifier Head):这是一个指向链表第一个节点的指针,例如 struct blocking_notifier_headstruct atomic_notifier_head。发布者持有这个头指针。
  2. 工作流程

    • 订阅(Register):关心某个事件的内核模块(订阅者)会创建一个 notifier_block,并调用相应的注册函数(如 blocking_notifier_chain_register)将其添加到特定事件的通知链中。 注册函数会根据优先级将新的 notifier_block 插入到链表的正确位置。
    • 发布(Call Chain):当事件发生时,事件源(发布者)会调用通知链的执行函数(如 blocking_notifier_call_chain)。 这个函数会遍历整个链表,依次调用每个 notifier_block 中注册的回调函数,并将事件类型和相关数据作为参数传递过去。
    • 取消订阅(Unregister):当模块不再关心事件时(例如模块卸载时),它会调用注销函数(如 blocking_notifier_chain_unregister)将自己的 notifier_block 从链表中移除。

它的主要优势体现在哪些方面?

  • 解耦(Decoupling):实现了事件发布者和订阅者之间的完全解耦,是其最大优势。
  • 可扩展性(Extensibility):新的内核模块可以很容易地通过注册自己的回调函数来“挂钩”到现有内核事件上,而无需修改事件源的代码。
  • 多路广播(One-to-Many):一个事件可以同时通知任意数量的订阅者。
  • 优先级控制:允许订阅者通过设置优先级来影响其回调函数的执行顺序。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 同步执行:通知链的调用是同步的。发布者在调用 notifier_call_chain 后,会阻塞直到所有回调函数执行完毕。如果任何一个回调函数执行时间过长,会拖慢发布者的执行流程。
  • 缺乏返回值:通知链主要用于单向的事件通知,发布者通常不关心或无法有效处理来自众多订阅者的返回值。虽然回调函数可以返回 NOTIFY_STOP 等来提前终止链的调用,但这是一种有限的交互。
  • 调试复杂性:当系统出现问题时,很难直接从代码层面看出某个事件会触发哪些模块的执行,需要通过动态调试或代码阅读来追踪整个调用链。
  • 不适用于数据交换:该机制不适合需要进行大量数据交换或请求-响应模式的场景。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

当一个内核子系统需要将状态变化广播给其他未知数量和身份的子系统时,通知链是首选的、标准的解决方案。

  • CPU热插拔:当一个CPU核心即将离线时,它需要通知调度器、定时器、中断控制器等多个子系统进行相应的准备工作。通过调用 cpu_notifier,所有注册过的子系统都能收到通知并执行清理操作。
  • 网络接口管理:当一块网卡被启用(up)时,IP地址管理模块需要为其配置地址,路由模块需要更新路由表,防火墙模块需要应用相关规则。这些都是通过订阅 netdev_notifier 来实现的。
  • 系统关机流程:在系统执行关机或重启前,需要通知文件系统同步数据、设备驱动程序进入安全状态等。reboot_notifier 提供了一个让各个模块执行最后清理工作的机会。

是否有不推荐使用该技术的场景?为什么?

  • 点对点通信:如果一个模块只需要与另一个特定的模块通信,使用直接函数调用或者更简单的回调函数指针机制会更高效、更直观。
  • 需要明确返回值:当一个模块调用另一个模块需要得到一个明确的、复杂的处理结果时,通知链不适用。应该使用函数调用。
  • 实时性要求极高:由于无法预知链上有多少回调以及它们的执行时间,对于有严格实时性要求的场景,需要谨慎使用,特别是阻塞型通知链。

对比分析

请将其 与 其他相似技术 进行详细对比。

在内核中,存在多种组件间通信的方式,通知链是其中一种。

特性 内核通知链 (Notifier Chains) 直接函数调用/回调指针 Tracepoints / kprobes
通信模型 发布-订阅 (一-对-多) 请求-响应 (一-对-一) 探测点 (Probing)
耦合度 。发布者和订阅者完全解耦。 。调用者和被调用者在编译时就确定了依赖关系。 极低。主要用于调试和性能分析,逻辑上完全分离。
主要用途 异步事件通知,状态变更广播。 功能实现,数据处理,获取结果。 动态追踪、调试、性能监控、获取内核内部状态。
性能开销 中等。涉及链表遍历和间接函数调用。原子通知链使用RCU优化了调用开销。 。就是一次标准的函数调用开销。 较高。涉及到中断处理和专门的框架,对性能敏感路径有影响。
扩展性 。任何模块都可以动态地加入或退出通知链。 。增加新的处理者需要修改调用点的代码。 。可以动态地在几乎任何内核函数上挂载探测点。
数据流向 单向。从发布者到订阅者。 双向。可以有复杂的参数和返回值。 单向。从内核探测点到追踪工具。

include/linux/notifier.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* 通知器链有四种类型:
*
* 原子通知器链:链回调在 interrupt/atomic/context 中运行。不允许阻止 Callouts。
* 阻塞通知器链:链回调在进程上下文中运行。允许阻止 Callouts。
* 原始通知程序链:对回调、注册或注销没有限制。 所有锁定和保护都必须由调用方提供。
* SRCU 通知链:阻塞通知链的一种变体,具有相同的限制。
*
* atomic_notifier_chain_register() 可以从原子上下文中调用,但 blocking_notifier_chain_register() 和 srcu_notifier_chain_register() 必须从进程上下文中调用。 相应的 _unregister() 例程也是如此。
*
* atomic_notifier_chain_unregister()、blocking_notifier_chain_unregister() 和 srcu_notifier_chain_unregister() _must not_从调用链中调用。
*
* SRCU 通知链是阻止通知链的另一种形式。它们使用 SRCU (Sleepable Read-Copy Update) 而不是 rw-semaphores 来保护链环。 这意味着 srcu_notifier_call_chain() 中的开销_非常_低:没有缓存反弹,也没有内存障碍。作为补偿,srcu_notifier_chain_unregister() 相当昂贵。当 SRCU 通知器链经常被调用但很少被删除时notifier_blocks应该使用 SRCU 通知器链。
*/

atomic_notifier_head 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
};

struct atomic_notifier_head {
spinlock_t lock;
struct notifier_block __rcu *head;
};

notifier_chain_register 注册通知器链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
* 通知器链核心例程。 下面导出的例程位于这些例程之上,并添加了适当的锁定。
*/
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;
}

ATOMIC_NOTIFIER_HEAD 原子通知器链头部

1
2
3
4
5
6
7
#define ATOMIC_NOTIFIER_INIT(name) {				\
.lock = __SPIN_LOCK_UNLOCKED(name.lock), \
.head = NULL }

#define ATOMIC_NOTIFIER_HEAD(name) \
struct atomic_notifier_head name = \
ATOMIC_NOTIFIER_INIT(name)

atomic_notifier_chain_register 原子通知器链注册

1
2
3
4
5
6
7
8
9
10
11
12
int atomic_notifier_chain_register(struct atomic_notifier_head *nh,
struct notifier_block *n)
{
unsigned long flags;
int ret;

spin_lock_irqsave(&nh->lock, flags);
ret = notifier_chain_register(&nh->head, n, false);
spin_unlock_irqrestore(&nh->lock, flags);
return ret;
}
EXPORT_SYMBOL_GPL(atomic_notifier_chain_register);

kernel/notifier.c

blocking_notifier_chain_register: 向阻塞式通知链中添加一个通知器

这是一个通用的内核API,用于向任意一个阻塞式通知链中添加订阅者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* blocking_notifier_chain_register - 向一个阻塞式通知链中添加一个通知器
* @nh: 指向阻塞式通知链头部的指针
* @n: 要添加到通知链中的新条目
*
* 添加一个通知器到阻塞式通知链.
* 必须在进程上下文中调用 (因为可能会休眠).
*
* 成功时返回0, 如果已存在则返回 %-EEXIST.
*/
int blocking_notifier_chain_register(struct blocking_notifier_head *nh,
struct notifier_block *n)
{
/*
* 调用内部实现函数 __blocking_notifier_chain_register.
* 第三个参数 'unique_priority' 被设置为 false, 意味着允许不同的通知器使用相同的优先级.
*/
return __blocking_notifier_chain_register(nh, n, false);
}
/*
* 导出此通用API的符号.
*/
EXPORT_SYMBOL_GPL(blocking_notifier_chain_register);

__blocking_notifier_chain_register: 阻塞式通知链注册的内部实现

该函数实现了阻塞式注册的核心逻辑,即在修改链表前获取锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
* 阻塞式通知链例程. 对链的所有访问都由一个读写信号量(rwsem)同步.
*/

static int __blocking_notifier_chain_register(struct blocking_notifier_head *nh,
struct notifier_block *n,
bool unique_priority)
{
int ret;

/*
* 这段代码会在系统启动的早期阶段被使用, 此时任务调度尚未完全工作,
* 并且中断必须保持禁用状态. 在这种时候, 我们绝不能调用 down_write() (它会导致休眠).
* unlikely() 是一个编译器优化提示, 告诉编译器这个分支不太可能发生.
*/
if (unlikely(system_state == SYSTEM_BOOTING))
/*
* 如果在系统启动阶段, 则绕过锁, 直接调用底层的、无锁的注册函数.
* 此时系统是单线程执行的, 不需要锁.
*/
return notifier_chain_register(&nh->head, n, unique_priority);

/*
* 获取读写信号量的写锁. 如果有其他任务持有读锁或写锁, 当前任务会休眠等待.
* 这确保了在修改链表期间, 没有其他任务可以读取或修改它.
*/
down_write(&nh->rwsem);
/*
* 调用底层的核心注册函数, 实际地将通知器添加到链表中.
* nh->head 是从阻塞式通知链头部结构体中取出原始的链表头.
*/
ret = notifier_chain_register(&nh->head, n, unique_priority);
/*
* 释放写锁, 允许其他任务访问此链.
*/
up_write(&nh->rwsem);
/*
* 返回底层函数的结果.
*/
return ret;
}

notifier_chain_register: 通知链的核心注册逻辑

这是所有通知链注册功能的最底层实现,它在一个无锁(但受RCU保护)的环境中操作链表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/*
* 通知链核心例程. 下面导出的例程都基于这些核心例程, 并添加了适当的锁.
*/

static int notifier_chain_register(struct notifier_block **nl,
struct notifier_block *n,
bool unique_priority)
{
/*
* 使用 while 循环遍历通知链. nl 是一个指向指针的指针, 它在链表中移动.
* 循环条件是: 只要当前指向的指针不为NULL, 就继续.
*/
while ((*nl) != NULL) {
/*
* 检查要注册的通知器(n)是否已经存在于链表中.
* unlikely() 提示编译器这是一个不常见的情况.
*/
if (unlikely((*nl) == n)) {
/*
* 如果已存在, 打印一个内核警告. %ps 是一个特殊的格式符, 用于打印符号(函数名).
*/
WARN(1, "notifier callback %ps already registered",
n->notifier_call);
/*
* 返回错误码 -EEXIST (已存在).
*/
return -EEXIST;
}
/*
* 检查新通知器(n)的优先级是否高于当前链表节点的优先级.
* 链表是按优先级降序排列的.
*/
if (n->priority > (*nl)->priority)
/*
* 如果是, 说明找到了正确的插入位置 (新节点应该插在当前节点之前), 退出循环.
*/
break;
/*
* 如果要求优先级唯一(unique_priority为true), 且新旧优先级相同.
*/
if (n->priority == (*nl)->priority && unique_priority)
/*
* 返回错误码 -EBUSY (资源忙), 表示该优先级已被占用.
*/
return -EBUSY;
/*
* 移动到下一个节点. nl 指向当前节点的 next 指针的地址.
*/
nl = &((*nl)->next);
}
/*
* 将新节点的 next 指针指向当前位置的节点 (即原来 nl 指向的节点).
*/
n->next = *nl;
/*
* 使用 rcu_assign_pointer() 原子地将新节点(n)的地址赋值给 nl 指向的位置.
* 这是RCU机制的关键写操作. 它确保了正在遍历此链表的读者能看到一个一致的旧链表或新链表,
* 而不会看到一个被破坏的中间状态.
*/
rcu_assign_pointer(*nl, n);
/*
* 调用跟踪函数, 这在启用内核跟踪(ftrace)时会记录此注册事件, 用于调试.
*/
trace_notifier_register((void *)n->notifier_call);
/*
* 返回0, 表示成功.
*/
return 0;
}

notifier_call_chain: 调用通知链以广播一个事件

该函数是Linux内核通知(Notifier)机制的核心执行器。它的根本作用是遍历一个给定的通知链(一个由notifier_block结构体组成的链表),并按优先级顺序调用链上每一个已注册的订阅者(notifier)所提供的回调函数,从而将一个特定的事件(由valv参数定义)广播出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/**
* notifier_call_chain - 通知已注册的订阅者一个事件的发生.
* @nl: 指向通知链头部的指针的指针.
* @val: 一个无符号长整型值, 会被原封不动地传递给通知回调函数. 通常用于表示事件的类型或状态.
* @v: 一个void指针, 也会被原封不动地传递给通知回调函数. 通常用于指向与事件相关的数据结构.
* @nr_to_call: 要调用的通知函数的数量. 如果是-1, 表示调用链上的所有函数.
* @nr_calls: 一个可选的指针, 用于记录实际发送的通知数量. 如果不关心, 可以是NULL.
* @Return: 返回最后一个被调用的通知函数所返回的值.
*/
static int notifier_call_chain(struct notifier_block **nl,
unsigned long val, void *v,
int nr_to_call, int *nr_calls)
{
/*
* 定义一个整型变量 ret, 用于存储回调函数的返回值.
* 初始化为 NOTIFY_DONE, 这是当链表为空或没有函数被调用时的默认返回值.
*/
int ret = NOTIFY_DONE;
/*
* 定义两个指向 notifier_block 的指针.
* nb 用于指向当前正在处理的节点.
* next_nb 用于预先存储下一个节点, 以增加遍历的稳健性.
*/
struct notifier_block *nb, *next_nb;

/*
* 使用 rcu_dereference_raw 获取链表的头节点.
* 这是RCU的读端操作, 它确保在RCU的读端临界区内, 获取到的指针 nb 是有效的,
* 即使此时有其他任务正在修改链表. 'raw'版本表示调用者需要负责确保RCU的读端锁已经被持有.
*/
nb = rcu_dereference_raw(*nl);

/*
* 循环遍历链表. 循环条件是: 当前节点(nb)不为NULL, 并且需要调用的数量(nr_to_call)尚未用尽.
*/
while (nb && nr_to_call) {
/*
* 预先获取下一个节点. 同样使用 rcu_dereference_raw 来安全地解引用 next 指针.
* 这是一个关键的健壮性设计: 即使 nb->notifier_call() 内部将 nb 自身从链表中移除,
* 我们依然有 next_nb 指针可以继续遍历.
*/
next_nb = rcu_dereference_raw(nb->next);

/*
* 这是一个条件编译块, 仅当内核配置了 CONFIG_DEBUG_NOTIFIERS 时生效, 用于调试.
*/
#ifdef CONFIG_DEBUG_NOTIFIERS
/*
* 检查回调函数的指针是否指向一个有效的内核代码段地址.
* 这可以帮助捕捉因内存损坏导致函数指针被覆盖的严重错误.
*/
if (unlikely(!func_ptr_is_kernel_text(nb->notifier_call))) {
WARN(1, "Invalid notifier called!"); // 如果指针无效, 打印内核警告.
nb = next_nb; // 跳过此无效节点.
continue; // 继续下一次循环.
}
#endif
/*
* 调用跟踪函数, 如果内核跟踪(ftrace)被启用, 这会记录一次通知回调的运行事件.
*/
trace_notifier_run((void *)nb->notifier_call);
/*
* 调用当前订阅者(nb)的回调函数(notifier_call), 并将事件类型(val)和数据(v)传递给它.
* 将回调函数的返回值保存在 ret 中.
*/
ret = nb->notifier_call(nb, val, v);

/*
* 如果调用者提供了 nr_calls 指针, 则将计数值加一.
*/
if (nr_calls)
(*nr_calls)++;

/*
* 检查回调函数的返回值. 如果返回值的 NOTIFY_STOP_MASK 位被设置 (例如, 返回了 NOTIFY_STOP),
* 这表示一个高优先级的订阅者已经完全处理了这个事件, 不希望链上后续的订阅者再处理它.
*/
if (ret & NOTIFY_STOP_MASK)
/*
* 如果是, 则中断遍历过程.
*/
break;
/*
* 将当前节点指针移到之前保存的下一个节点, 准备处理链表中的下一个订阅者.
*/
nb = next_nb;
/*
* 将待调用计数器减一.
*/
nr_to_call--;
}
/*
* 返回最后一个被调用的回调函数的返回值.
*/
return ret;
}
/*
* NOKPROBE_SYMBOL 是一个宏, 它将此函数标记为不可被 kprobe 探测.
* kprobe是内核的动态探测机制. 对于一些非常核心或性能敏感的底层函数,
* 会禁止探测以防止潜在的递归或不稳定问题.
*/
NOKPROBE_SYMBOL(notifier_call_chain);

blocking_notifier_call_chain: 调用一个阻塞式通知链中的所有函数

这是一个通用的内核API,用于触发任何一个阻塞式通知链。它负责处理锁机制,并调用底层的notifier_call_chain来实际遍历和执行回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* blocking_notifier_call_chain - 调用一个阻塞式通知链中的函数
* @nh: 指向阻塞式通知链头部的指针
* @val: 被原封不动传递给通知函数的值
* @v: 被原封不动传递给通知函数的指针
*
* 依次调用通知链中的每一个函数. 这些函数在进程上下文中运行,
* 因此它们被允许阻塞(休眠).
*
* 如果通知回调的返回值可以与 %NOTIFY_STOP_MASK 进行'与'操作为真,
* 那么 blocking_notifier_call_chain() 将会立即返回,
* 其返回值就是那个中止了执行的通知函数的返回值.
* 否则, 返回值就是最后一个被调用的通知函数的返回值.
*/
int blocking_notifier_call_chain(struct blocking_notifier_head *nh,
unsigned long val, void *v)
{
/*
* 定义一个整型变量 ret, 用于存储返回值, 默认为 NOTIFY_DONE.
*/
int ret = NOTIFY_DONE;

/*
* 在获取锁之前, 我们先检查链表的头部. 即使这个访问是竞争的(racy), 也没有关系,
* 因为无论测试结果如何, 我们在获取锁之后都会重新检查链表.
* 这是一种性能优化: 如果链表为空, 就可以避免获取和释放锁的开销.
* rcu_access_pointer() 用于在不持有RCU读端锁的情况下, 安全地检查指针是否为NULL.
*/
if (rcu_access_pointer(nh->head)) {
/*
* 获取读写信号量(rwsem)的读锁.
* 这允许多个调用者同时读取(即调用)这个链, 但会阻塞任何试图写入(注册/注销)的调用者.
* 在单核抢占式内核中, 如果当前任务被一个更高优先级的、想要获取写锁的任务抢占,
* 那么高优先级任务会休眠, 直到读锁被释放.
*/
down_read(&nh->rwsem);
/*
* 调用无锁的底层通知链执行函数.
* &nh->head: 传递原始链表的头指针.
* val, v: 传递事件值和数据.
* -1: 表示调用链上的所有订阅者.
* NULL: 表示我们不关心实际调用的数量.
*/
ret = notifier_call_chain(&nh->head, val, v, -1, NULL);
/*
* 释放读锁, 允许被阻塞的写者继续执行.
*/
up_read(&nh->rwsem);
}
/*
* 返回执行结果.
*/
return ret;
}
/*
* 将此通用API的符号导出, 以便其他GPL许可证的内核模块可以使用.
*/
EXPORT_SYMBOL_GPL(blocking_notifier_call_chain);