[TOC]

kernel/async.c 内核异步函数调用(Asynchronous Function Calls) 一个简单的“发后即忘”式内核执行框架

历史与背景

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

这项技术以及其实现的异步函数调用(async)框架,是为了解决内核中一个特定的并发需求:以最简单的方式,将一个函数的执行推迟到一个独立的上下文中,而调用者无需等待其完成

  • 优化启动时间:这是async框架诞生的最主要驱动力。在内核启动过程中,有大量的初始化调用(initcalls)。如果严格按照顺序依次执行,会非常耗时。async框架允许内核将那些没有严格依赖关系的、耗时的初始化函数“并行化”,将它们提交到后台异步执行,而主启动流程可以继续进行。这显著缩短了系统的总启动时间。
  • 降低关键路径延迟:在某些性能敏感的代码路径中(例如一个设备驱动的.probe函数),可能会有一些耗时但非必需的初始化步骤。通过async,可以将这些步骤“发后即忘”(fire-and-forget)地交由后台处理,使得关键路径函数可以更快地返回,提高系统响应速度。
  • 提供比工作队列更简单的接口:内核已经有了强大的工作队列(workqueue)机制用于延迟工作。但对于最简单的“我只想让这个函数在别处运行,别的什么都不关心”的场景,设置一个work_struct、初始化、然后schedule_work的流程还是显得有些繁琐。async提供了一个极其简单的单一函数调用接口(async_schedule),作为工作队列的一个轻量级封装。

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

async框架是一个小而精的工具,其发展主要体现在功能的完善上,而非大的架构变革。

  • 基本实现async最初被引入时,提供了核心的async_schedule功能,它本质上是对全局工作队列的一个简单封装。
  • 同步点的加入:很快,开发者意识到纯粹的“发后即忘”在很多场景下是不够的。例如,在启动过程中,虽然很多initcalls可以并行执行,但在进入下一个大的启动阶段之前,必须确保之前所有的initcalls都已经完成。为此,内核加入了同步功能,即async_cookie_tasync_synchronize_cookie() / async_synchronize_full()。这使得调用者可以在一个稍后的、合适的“检查点”等待一个或所有已提交的异步任务完成。

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

async是一个非常稳定、成熟,但相对小众的内核组件。

  • 社区活跃度:其代码库非常稳定,几乎没有改动。它被认为是“已完成”的功能,维护工作仅限于必要的bug修复。
  • 主流应用
    • 内核启动:这是它最重要和最著名的应用场景。在do_initcalls()中,内核会使用异步模式来并行执行模块初始化。
    • 设备探测:在一些复杂的子系统(如component框架)或驱动中,可能会用它来异步执行一些探测或绑定的收尾工作。
    • 总体而言,它的使用场景远不如工作队列(workqueues)广泛,主要局限于那些需要批量并行化、且后续有明确同步点的流程。

核心原理与设计

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

async的核心原理是作为内核全局工作队列的一个极简主义前端封装

  1. 数据结构:核心是一个小的内部控制结构async_entry。它包含了要执行的函数指针、传递给该函数的参数、一个用于同步的completion对象,以及一个work_struct
  2. 调度 (async_schedule):当内核代码调用async_schedule(func, data)时,会发生以下事情:
    • 从一个预先分配的slab缓存中获取一个async_entry对象。
    • 将传入的funcdata存入这个对象。
    • 初始化该对象中的completionwork_struct
    • 最关键的一步:调用schedule_work(),将这个async_entry中的work_struct放入内核的全局系统工作队列system_wq)中。
    • 函数返回一个async_cookie_t,它实际上就是这个async_entry的地址。
  3. 执行:稍后,一个内核的worker线程会从system_wq中取出这个work_struct并执行其处理函数。这个处理函数(async_run_entry)的作用很简单:
    • work_struct中找到async_entry的地址。
    • 调用async_entry中存储的func指针,并传入data参数。
    • 函数执行完毕后,调用complete()来唤醒任何可能在等待这个任务完成的代码。
  4. 同步
    • async_synchronize_cookie(cookie):这个函数会调用wait_for_completion(),等待cookie所代表的那个async_entry中的completion对象被信号。
    • async_synchronize_full():这个函数会遍历所有当前正在运行的异步任务,并逐个等待它们全部完成。

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

  • 极致的简单:API只有一个核心函数async_schedule,使用起来非常直观,隐藏了所有工作队列的实现细节。
  • 低开销:对于调用者来说,开销非常小,只是一个函数调用和一次轻量级的内存分配。
  • 内置同步:提供了简单的机制来等待任务完成,非常适合“并行化-同步点”模型。

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

  • 使用全局队列:所有async任务都共享同一个全局工作队列。这意味着无法进行优先级区分,也无法实现任务间的隔离。一个行为不当的异步任务可能会影响到系统中所有其他使用async框架的任务。
  • 功能单一:它不支持延迟执行、不支持指定在特定CPU上运行、不支持限制并发数。
  • 缺乏错误处理:这是一个“发后即忘”的框架。被调用的函数执行成功与否,原始的调用者无法直接获知。

使用场景

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

当需要简单地、批量地并行化执行一系列无依赖的、非关键路径的函数时,async是首选方案。

  • 内核初始化:这是教科书式的例子。在init/main.c中,do_initcalls()在适当的阶段会调用do_initcall_async()。这个函数会遍历一个initcall列表,为每个函数调用async_schedule()。在所有调用都提交后,它会调用async_synchronize_full()来等待它们全部结束,然后再继续下一阶段的启动。
  • 简化驱动探测:假设一个驱动在探测时需要执行一个耗时的固件加载操作,但这个操作对于探测成功不是必需的。驱动可以在其.probe函数中调用async_schedule(load_optional_firmware, dev),然后立即返回成功。这使得设备可以更快地变为可用状态。

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

  • 需要精细控制的任何场景:如果你需要控制任务的优先级、并发数,或者希望任务在特定的CPU上运行,那么应该使用工作队列(Workqueues)。工作队列允许你创建自己的、具有特定属性的队列。
  • 需要长期运行的任务或守护进程async用于短暂的、一次性的函数调用。对于需要作为后台服务长期运行的任务,应该创建专门的内核线程(kthreads)
  • 需要与硬件中断交互的延迟工作:对于中断处理的下半部(bottom-half),应该使用tasklets或softirqs,它们提供了更严格的执行上下文保证。

对比分析

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

特性 异步函数调用 (async.c) 工作队列 (Workqueues) 内核线程 (kthreads)
实现方式/抽象层级 高层封装。是对全局工作队列的极简包装。 中层框架。一个通用的、功能丰富的延迟工作执行框架。 底层原语。一个完整的、可独立调度的内核执行线程。
使用复杂度 极低。单一函数调用async_schedule() 。需要定义work_struct并初始化。 。需要编写线程主函数,并手动处理线程的创建、同步和退出。
资源开销 极低。仅一个小的控制结构async_entry 。一个work_struct的开销。 。一个完整的task_struct、内核栈等。
控制粒度 。所有任务共享全局队列,无优先级、无隔离。 。可以创建私有工作队列,设置并发数、NUMA亲和性等。 完全控制。可以设置调度策略、优先级,并管理自己的状态。
适用场景 简单的、批量的“发后即忘”式并行化,尤其适用于启动过程。 通用的、需要延迟或异步执行的任务,是内核中最常用的延迟机制。 长期运行的后台任务、守护进程或需要复杂状态管理的并发任务。

async_init 为内核的异步函数调用(async)框架创建一个专门的、非绑定的(unbound)工作队列(workqueue)

之所以需要一个专门的工作队列,是因为异步框架可能会同时调度大量相互依赖的任务。如果使用默认的共享非绑定工作队列,其并发度(由min_active参数控制)可能不足,导致任务之间因等待资源而产生死锁或停滞(stall)。通过创建一个名为 “async” 的私有工作队列,并为其设置一个较高的最小活跃工作线程数(min_active),可以确保异步框架有足够的并发能力来处理这些复杂依赖的任务,从而保证系统的稳定性和性能。

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
// 定义内核异步功能初始化函数
// __init 宏表示该函数仅在内核初始化阶段执行,执行完毕后其占用的内存会被释放。
void __init async_init(void)
{
/*
* 异步调用(Async)框架可能会调度许多相互依赖的工作项。然而,
* 非绑定的工作队列最多只能处理 min_active 个相互依赖的工作项。
* 默认的 min_active 值(通常是8)对于异步框架来说是不够的,
* 并且可能导致系统停滞。因此,我们使用一个专门的、提高了
* min_active 值的工作队列。
*/

// 分配一个新的工作队列,并将其指针赋值给全局变量 async_wq。
// "async": 工作队列的名称,会显示在内核日志或进程列表中。
// WQ_UNBOUND: 标志,表示这是一个非绑定的工作队列,其工作线程不与特定CPU绑定。
// 0: max_active 参数,设置为0表示使用默认的每个CPU最大活跃工作项数。
async_wq = alloc_workqueue("async", WQ_UNBOUND, 0);

// 这是一个断言宏。检查 async_wq 是否为 NULL(即 alloc_workqueue 是否失败)。
// 如果工作队列分配失败,这是一个致命错误,内核将打印错误信息并停止运行(panic)。
BUG_ON(!async_wq);

// 设置 async_wq 工作队列的“最小活跃工作线程数”。
// WQ_DFL_ACTIVE 是一个预定义的常量,其值通常远大于默认的最小值,
// 以确保该工作队列有足够多的并发工作线程。
workqueue_set_min_active(async_wq, WQ_DFL_ACTIVE);
}

async_run_entry_fn: 执行并清理异步任务

此函数是内核异步工作队列 (workqueue) 的实际执行体. 当内核的工作线程 (kworker) 从异步工作队列 async_wq 中取出一个待处理的工作项 (work_struct) 时, 就会调用这个函数. 它的核心职责是: 执行用户最初请求异步调用的函数, 然后清理与该任务相关的所有资源, 并通知其他可能正在等待的内核部分.

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
103
104
105
106
/*
* 静态函数声明: async_run_entry_fn
* 此函数作为工作队列的回调函数, 用于执行一个挂起的异步任务.
* @work: 指向 struct work_struct 的指针, 这是由工作队列子系统传递过来的.
* 这个 work 结构体是某个 async_entry 的一部分.
*/
static void async_run_entry_fn(struct work_struct *work)
{
/*
* 使用 container_of 宏, 从 work 成员的地址反向计算出其所属的父结构体 async_entry 的起始地址.
* 这是内核中从一个内嵌结构体指针获取容器结构体指针的标准做法.
* 变量 entry 现在指向代表当前要执行的整个异步任务的实体.
*/
struct async_entry *entry =
container_of(work, struct async_entry, work);
/*
* 定义一个无符号长整型变量 flags, 用于保存调用 spin_lock_irqsave 时的CPU中断状态.
*/
unsigned long flags;
/*
* 定义一个 ktime_t 类型的变量 calltime. ktime_t 用于存储内核的高精度时间戳.
* 它将用于记录异步函数的执行时长, 以便调试.
*/
ktime_t calltime;

/*
* 步骤 1: 执行用户函数 (并打印调试信息)
*/

/*
* 使用 pr_debug 打印一条调试日志. 这类日志只有在内核配置了动态调试并且被启用时才会输出.
* %lli: 用于打印 long long 类型的 entry->cookie (任务ID).
* %pS: 用于打印 entry->func 指针所指向的函数名.
* %i: 用于打印当前任务的PID. task_pid_nr(current) 返回当前正在执行任务(即 kworker 内核线程)的PID.
*/
pr_debug("calling %lli_%pS @ %i\n", (long long)entry->cookie,
entry->func, task_pid_nr(current));
/*
* 调用 ktime_get() 获取当前的高精度时间戳, 记录函数开始执行的时刻.
*/
calltime = ktime_get();

/*
* 这是核心的执行步骤: 通过函数指针 entry->func 调用用户提交的原始函数.
* 同时, 将用户提供的原始数据 entry->data 和本次任务的唯一标识符 entry->cookie 作为参数传递过去.
*/
entry->func(entry->data, entry->cookie);

/*
* 再次使用 pr_debug 打印一条调试日志, 报告函数执行完毕.
* microseconds_since(calltime) 会计算从 calltime 时刻到现在所经过的微秒数.
*/
pr_debug("initcall %lli_%pS returned after %lld usecs\n",
(long long)entry->cookie, entry->func,
microseconds_since(calltime));

/*
* 步骤 2: 将任务实体从各个待处理队列中移除.
*/

/*
* 获取全局自旋锁 async_lock, 并禁用当前CPU的中断, 保存中断状态到 flags.
* 这是为了保护对全局共享链表(domain_list, global_list)和计数器(entry_count)的访问,
* 防止在修改过程中被中断或其他任务抢占, 从而确保操作的原子性.
*/
spin_lock_irqsave(&async_lock, flags);
/*
* 调用 list_del_init 从其所属的同步域的 pending 链表中移除 entry.
* _init 版本在删除节点后, 会重新初始化该节点的链表头, 使其指向自身, 这是一个安全措施.
*/
list_del_init(&entry->domain_list);
/*
* 从全局的 async_global_pending 链表中移除 entry.
*/
list_del_init(&entry->global_list);

/*
* 步骤 3: 释放任务实体占用的资源.
*/

/*
* 调用 kfree 释放 entry 结构体本身占用的内存.
* 这块内存是在 __async_schedule_node_domain 函数中用 kzalloc 分配的.
*/
kfree(entry);
/*
* 原子地将全局异步任务计数器 entry_count 的值减一.
*/
atomic_dec(&entry_count);

/*
* 释放自旋锁, 并恢复之前保存的中断状态. 临界区结束.
*/
spin_unlock_irqrestore(&async_lock, flags);

/*
* 步骤 4: 唤醒任何可能正在等待的进程.
*/

/*
* 调用 wake_up 唤醒在 async_done 等待队列上睡眠的所有进程.
* 例如, 如果有进程调用了 async_synchronize_full() 或类似的同步函数,
* 它们就会在这个等待队列上睡眠, 等待所有(或特定)异步任务完成.
*/
wake_up(&async_done);
}

__async_schedule_node_domain: 异步任务调度的核心实现

此函数是Linux内核异步执行框架的内部核心, 负责将一个已经准备好的异步任务实体(entry)加入到相应的等待队列, 并最终提交给内核工作队列(workqueue)去执行. 它是 async_schedule_node_domain 函数在成功分配了任务实体后的实际执行者.

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
103
104
105
106
107
108
/*
* 静态函数声明: __async_schedule_node_domain
* 这是异步调度的内部核心函数, 它假定 entry 结构体已经被成功分配.
*
* @func: 需要异步执行的函数指针.
* @data: 将要传递给 func 函数的 void 指针.
* @node: 期望执行此任务的NUMA节点. 在STM32H750上此参数无效.
* @domain: 此任务所属的同步域.
* @entry: 由调用者预先分配并传入的, 用于封装此异步任务的 async_entry 结构体指针.
* @return: 返回一个唯一的 async_cookie_t 任务标识符.
*/
static async_cookie_t __async_schedule_node_domain(async_func_t func,
void *data, int node,
struct async_domain *domain,
struct async_entry *entry)
{
/*
* 定义一个 async_cookie_t 类型的变量 newcookie, 用于存储新分配的任务标识符.
*/
async_cookie_t newcookie;
/*
* 定义一个无符号长整型变量 flags, 用于在调用自旋锁时保存当前的中断状态(IRQs).
*/
unsigned long flags;

/*
* 调用 INIT_LIST_HEAD 宏来初始化 entry 中的 domain_list 成员.
* list_head 是Linux内核链表的节点. 初始化后, 它指向自身, 表示一个空链表.
* 这个链表用于将 entry 连接到其所属 domain 的 pending 链表中.
*/
INIT_LIST_HEAD(&entry->domain_list);
/*
* 初始化 entry 中的 global_list 成员.
* 这个链表用于将 entry 连接到全局的 async_global_pending 链表中.
*/
INIT_LIST_HEAD(&entry->global_list);
/*
* 调用 INIT_WORK 宏来初始化 entry 中的 work 成员 (一个 struct work_struct).
* 这是将异步任务与内核工作队列(workqueue)机制关联起来的关键一步.
* 它将 work 结构体与一个处理函数 async_run_entry_fn 绑定.
* 当工作队列执行这个 work 时, async_run_entry_fn 函数就会被调用.
*/
INIT_WORK(&entry->work, async_run_entry_fn);
/*
* 将调用者提供的函数指针 func 保存到 entry 结构体中.
*/
entry->func = func;
/*
* 将调用者提供的参数数据指针 data 保存到 entry 结构体中.
*/
entry->data = data;
/*
* 将此任务所属的同步域 domain 指针保存到 entry 结构体中.
*/
entry->domain = domain;

/*
* 获取全局的异步框架自旋锁 async_lock, 并禁用本地中断, 将当前的中断状态保存到 flags 变量中.
* 在单核系统上, 这能防止当前代码被中断处理程序或其他任务抢占, 从而保护接下来的共享数据访问.
*/
spin_lock_irqsave(&async_lock, flags);

/*
* 从全局的 cookie 计数器 next_cookie 中分配一个唯一的新ID.
* 这个ID同时赋值给 entry->cookie (用于内部记录) 和 newcookie (用于返回给调用者).
* next_cookie++ 保证了下一次分配的ID是不同的.
*/
newcookie = entry->cookie = next_cookie++;

/*
* 将当前任务实体 entry 添加到其所属同步域 domain 的 pending 链表的尾部.
* 这个链表用于实现针对特定域的同步等待.
*/
list_add_tail(&entry->domain_list, &domain->pending);
/*
* 检查当前任务的同步域 domain 是否被注册到了全局同步机制中.
*/
if (domain->registered)
/*
* 如果 domain 已注册, 那么也将此任务实体 entry 添加到全局待处理链表 async_global_pending 的尾部.
* 这个链表用于实现全局同步, 如 async_synchronize_full().
*/
list_add_tail(&entry->global_list, &async_global_pending);

/*
* 原子地增加全局异步任务计数器 entry_count 的值.
* atomic_inc 是一个原子操作, 确保计数的准确性, 不受中断或抢占影响.
*/
atomic_inc(&entry_count);
/*
* 释放 async_lock 自旋锁, 并恢复到进入临界区之前的中断状态 (由 flags 变量记录).
*/
spin_unlock_irqrestore(&async_lock, flags);

/*
* 调用 queue_work_node 函数, 正式将任务提交给内核的工作队列子系统去执行.
* node: 期望的NUMA节点. 在STM32H750上, 此参数被忽略.
* async_wq: 指向异步框架专用的工作队列(struct workqueue_struct)的指针.
* &entry->work: 要提交的具体工作项. 内核的工作线程(kworker)会适时执行与这个工作项关联的函数,
* 也就是我们之前设置的 async_run_entry_fn.
*/
queue_work_node(node, async_wq, &entry->work);

/*
* 向最初的调用者返回本次异步任务的唯一标识符.
*/
return newcookie;
}

async_schedule_node_domain: 在指定的NUMA节点上使用特定域调度一个异步函数执行

此函数是内核异步调度框架的核心实现. 它的主要作用是接收一个函数指针(func)和其参数(data), 并将它们打包成一个异步工作项, 然后提交给内核的工作队列, 以便稍后由内核线程执行. 它允许指定一个同步域(domain), 用于后续更细粒度的同步等待.

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
103
104
105
106
/**
* async_cookie_t async_schedule_node_domain(async_func_t func, void *data,
* int node, struct async_domain *domain)
* @func: 需要异步执行的函数指针.
* @data: 一个将要传递给 func 函数的 void 指针, 用于传递任意数据.
* @node: 期望在其上或其附近调度此异步任务的NUMA节点. 在STM32H750上此参数无效.
* @domain: 指向一个 async_domain 结构体的指针, 用于将异步任务分组, 以便后续可以只同步这个域内的任务.
*
* @return: 返回一个 async_cookie_t 类型的值, 这是一个唯一的标识符, 后续可用于检查点或等待此特定任务完成.
*
* 注意: 此函数可以在原子上下文(如中断处理程序)或非原子上下文中调用.
*
* 对于指定的节点, 内核会尽力满足. 如果该节点没有关联的CPU, 工作将在所有可用的CPU之间分发.
* (在STM32H750上, 只有一个CPU, 所以总是在其上执行).
*/
async_cookie_t async_schedule_node_domain(async_func_t func, void *data,
int node, struct async_domain *domain)
{
/*
* 定义一个指向 async_entry 结构体的指针. async_entry 用于封装一次异步执行所需的所有信息.
*/
struct async_entry *entry;
/*
* 定义一个无符号长整型变量 flags, 用于在调用自旋锁时保存当前的中断状态.
*/
unsigned long flags;
/*
* 定义一个 async_cookie_t 类型的变量 newcookie. async_cookie_t 本质上是一个整数类型,
* 用于唯一标识每一个异步任务.
*/
async_cookie_t newcookie;

/*
* 使用 kzalloc 分配一个 async_entry 结构体的内存.
* kzalloc 会将分配的内存区域清零.
* GFP_ATOMIC 标志位至关重要: 它指示内存分配必须在原子上下文中进行,
* 这意味着分配过程不能睡眠(阻塞). 这使得本函数可以安全地从中断处理程序中调用.
*/
entry = kzalloc(sizeof(struct async_entry), GFP_ATOMIC);

/*
* 检查两种可能导致异步执行失败的情况:
* 1. !entry: 上一步的内存分配失败 (系统内存极度紧张).
* 2. atomic_read(&entry_count) > MAX_WORK: 全局的异步任务计数器(entry_count)的值已经超过了设定的最大值(MAX_WORK).
* 这是一个保护机制, 防止过多的异步任务被提交, 耗尽系统资源. atomic_read 用于原子地读取计数器值.
* 如果任一条件为真, 则无法进行异步调度, 将转而以同步方式执行.
*/
if (!entry || atomic_read(&entry_count) > MAX_WORK) {
/*
* 如果是因为任务队列已满(entry分配成功), 则需要先释放掉刚刚分配的内存.
*/
kfree(entry);
/*
* 进入一个临界区来安全地获取一个唯一的任务cookie.
* spin_lock_irqsave 会获取一个自旋锁(async_lock)并禁用当前CPU的中断.
* 在单核STM32H750上, 这主要用于防止中断处理程序并发地修改 next_cookie.
*/
spin_lock_irqsave(&async_lock, flags);
/*
* 从全局的 next_cookie 计数器中获取一个新值, 并将计数器加一.
*/
newcookie = next_cookie++;
/*
* 释放自旋锁, 并恢复到调用spin_lock_irqsave之前的CPU中断状态.
*/
spin_unlock_irqrestore(&async_lock, flags);

/*
* 由于内存不足或任务队列已满, 无法进行异步调度,
* 因此直接在当前的上下文中调用该函数, 实现同步执行.
*/
func(data, newcookie);
/*
* 返回新生成的cookie. 即便任务是同步执行的, 仍然提供一个cookie.
*/
return newcookie;
}

/*
* 如果内存分配成功且任务队列未满, 则调用内部函数 __async_schedule_node_domain
* 来完成实际的异步调度工作(即将entry添加到工作队列中).
*/
return __async_schedule_node_domain(func, data, node, domain, entry);
}
/*
* 将 async_schedule_node_domain 函数导出, 以便遵循GPL协议的内核模块可以使用它.
*/
EXPORT_SYMBOL_GPL(async_schedule_node_domain);
/**
* async_schedule_domain - schedule a function for asynchronous execution within a certain domain
* @func: function to execute asynchronously
* @data: data pointer to pass to the function
* @domain: the domain
*
* Returns an async_cookie_t that may be used for checkpointing later.
* @domain may be used in the async_synchronize_*_domain() functions to
* wait within a certain synchronization domain rather than globally.
* Note: This function may be called from atomic or non-atomic contexts.
*/
static inline async_cookie_t
async_schedule_domain(async_func_t func, void *data,
struct async_domain *domain)
{
return async_schedule_node_domain(func, data, NUMA_NO_NODE, domain);
}

async_schedule_node: 在指定的NUMA节点上使用默认域调度异步函数

此函数是 async_schedule_node_domain 的一个简化版本. 它隐藏了 domain (同步域) 的概念, 总是使用系统默认的同步域 (async_dfl_domain). 这为那些不需要复杂同步控制的调用者提供了便利.

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
/**
* async_cookie_t async_schedule_node(async_func_t func, void *data, int node)
* @func: 需要异步执行的函数指针.
* @data: 一个将要传递给 func 函数的 void 指针, 用于传递任意数据.
* @node: 期望在其上或其附近调度此异步任务的NUMA节点. 在STM32H750上此参数无效.
*
* @return: 返回一个 async_cookie_t 类型的值, 后续可用于检查点或等待此特定任务完成.
* 注意: 此函数可以在原子上下文(如中断处理程序)或非原子上下文中调用.
*
* 对于指定的节点, 内核会尽力满足. 如果该节点没有关联的CPU, 工作将在所有可用的CPU之间分发.
*/
async_cookie_t async_schedule_node(async_func_t func, void *data, int node)
{
/*
* 此函数是 async_schedule_node_domain 的一个简单封装.
* 它直接调用前者, 并为其 domain 参数传递一个全局的默认域 &async_dfl_domain.
* 这意味着所有通过 async_schedule 和 async_schedule_node 调度的任务都属于同一个默认同步域,
* 可以通过调用 async_synchronize_full() 来等待所有这些默认域中的任务完成.
*/
return async_schedule_node_domain(func, data, node, &async_dfl_domain);
}
/*
* 将 async_schedule_node 函数导出, 以便遵循GPL协议的内核模块可以使用它.
*/
EXPORT_SYMBOL_GPL(async_schedule_node);

async_schedule_dev: 在与设备关联的NUMA节点上调度异步函数

这是一个静态内联函数, 提供了更高层次的封装, 主要面向设备驱动程序的开发者. 它使得为特定设备调度一个异步任务变得非常简单, 因为它会自动从设备结构体 (struct device) 中推断出 NUMA 节点信息, 并将设备指针自身作为参数传递给异步执行的函数.

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
/**
* async_cookie_t async_schedule_dev(async_func_t func, struct device *dev)
* @func: 需要异步执行的函数指针.
* @dev: 设备指针, 它既作为参数传递给func, 也用于确定在哪个NUMA节点上运行函数.
*
* @return: 返回一个 async_cookie_t 类型的值, 后续可用于检查点或等待此特定任务完成.
*
* 通过这样做, 我们可以尝试在最靠近设备的CPU上对设备进行操作, 以期达到最佳效果.
* (在STM32H750上, 因为没有"远近"之分, 所以此优化无效, 但异步执行机制仍有效).
* 注意: 此函数可以在原子上下文(如中断处理程序)或非原子上下文中调用.
*/
static inline async_cookie_t
async_schedule_dev(async_func_t func, struct device *dev)
{
/*
* static inline: 这是一个C语言关键字组合.
* static: 表示此函数的作用域仅限于当前文件.
* inline: 建议编译器在调用点将此函数展开, 以减少函数调用的开销.
*
* 此函数调用了 async_schedule_node, 并为其传递了三个参数:
* 1. func: 需要异步执行的函数, 由调用者提供.
* 2. dev: 设备指针. 它被作为 void *data 参数传递. 这意味着异步函数 func 将会收到这个设备指针作为其第一个参数.
* 3. dev_to_node(dev): 调用 dev_to_node() 函数来从设备结构体中获取其关联的NUMA节点号.
* 在非NUMA的STM32H750系统上, dev_to_node(dev) 通常返回-1或0, 最终调度策略会退化为在唯一可用的CPU上执行.
*/
return async_schedule_node(func, dev, dev_to_node(dev));
}

lowest_in_progress: 查找进行中任务的最低Cookie值

此函数是内核异步同步机制 (async_synchronize_*) 的核心辅助函数, 它的唯一作用是查找并返回当前所有正在排队或执行的异步任务中, 拥有最小 cookie 值的那个 cookie.

它的工作原理基于一个简单而高效的设计:

  1. 所有新提交的异步任务, 都会被添加到相应待处理链表 (pendingglobal_pending) 的尾部.
  2. cookie 值是单调递增的.
  3. 因此, 待处理链表的头部的那个任务, 必然是所有待处理任务中最早被提交的, 也就拥有最低的 cookie 值.
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
/*
* 静态函数声明: lowest_in_progress
* 此函数仅在当前文件中可见.
* @domain: 指向一个 async_domain 结构体的指针. 如果不为NULL, 则只在此域内查找;
* 如果为NULL, 则在全局范围内查找.
* @return: 返回找到的进行中任务的最低cookie值. 如果没有任何进行中的任务, 则返回 ASYNC_COOKIE_MAX.
*/
static async_cookie_t lowest_in_progress(struct async_domain *domain)
{
/*
* 定义一个指向 async_entry 的指针 first, 用于存储找到的第一个待处理任务. 初始化为NULL.
*/
struct async_entry *first = NULL;
/*
* 定义一个 async_cookie_t 类型的变量 ret, 作为返回值.
* 将其初始化为 ASYNC_COOKIE_MAX, 这是cookie可能的最大值.
* 如果后续没有找到任何进行中的任务, 这个值将被直接返回.
*/
async_cookie_t ret = ASYNC_COOKIE_MAX;
/*
* 定义一个无符号长整型变量 flags, 用于为自旋锁保存CPU中断状态.
*/
unsigned long flags;

/*
* 获取全局异步框架的自旋锁 async_lock, 并禁用本地中断, 将中断状态存入 flags.
* 这创建了一个临界区, 以保证对下面的全局/域内链表的访问是原子的.
*/
spin_lock_irqsave(&async_lock, flags);

/*
* 检查调用者是否提供了一个具体的域.
*/
if (domain) {
/*
* 如果提供了域, 则检查此域的 pending (待处理)链表是否为空.
*/
if (!list_empty(&domain->pending))
/*
* 如果链表不为空, 则使用 list_first_entry 获取链表的第一个条目.
* 因为新任务总是添加到链表尾部, 所以第一个条目必然拥有最低的cookie值.
* 将这个条目的地址存入 first 指针.
*/
first = list_first_entry(&domain->pending,
struct async_entry, domain_list);
} else {
/*
* 如果没有提供域 (domain 为 NULL), 则检查全局的 async_global_pending 链表是否为空.
*/
if (!list_empty(&async_global_pending))
/*
* 如果全局链表不为空, 则获取其第一个条目.
* 这个条目拥有所有已注册域中待处理任务的最低cookie值.
*/
first = list_first_entry(&async_global_pending,
struct async_entry, global_list);
}

/*
* 检查我们是否成功找到了一个条目 (即 first 是否不再是 NULL).
*/
if (first)
/*
* 如果找到了, 就将返回值 ret 更新为这个条目的 cookie 值.
*/
ret = first->cookie;

/*
* 释放自旋锁, 并恢复之前保存的中断状态. 临界区结束.
*/
spin_unlock_irqrestore(&async_lock, flags);
/*
* 返回最终结果.
*/
return ret;
}

async_synchronize_full: 同步(等待)所有异步函数调用完成

此函数是内核异步框架中最强力的一个全局同步点. 调用此函数的任务将会进入睡眠, 直到系统中所有被调度执行的、尚未完成的异步任务全部执行完毕. 它是一个全局屏障, 确保在它返回后, 整个异步子系统处于静默状态.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* void async_synchronize_full(void) - 同步所有的异步函数调用
*
* 这个函数会等待, 直到所有的异步函数调用都已经完成.
*/
void async_synchronize_full(void)
{
/*
* 此函数是 async_synchronize_full_domain 的一个简单封装.
* 它直接调用后者, 并为其 domain 参数传递 NULL.
* 在 async_synchronize_full_domain 中, NULL 是一个特殊值,
* 意味着"同步所有已注册的域", 从而实现了全局同步.
*/
async_synchronize_full_domain(NULL);
}
/*
* 将 async_synchronize_full 函数导出, 供GPL许可的内核模块使用.
*/
EXPORT_SYMBOL_GPL(async_synchronize_full);

async_synchronize_full_domain: 同步特定域内的所有异步函数调用

这是一个更具针对性的同步函数. 它允许调用者只等待某个特定”同步域” (domain) 内的所有异步任务完成, 而不是等待全局所有的任务. 这在大型子系统中很有用, 这些子系统可能定义了自己的域, 以便能独立地管理和同步自己的后台任务, 而不受其他子系统的影响.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* async_synchronize_full_domain - 同步某个特定域内的所有异步函数
* @domain: 需要同步的域.
*
* 这个函数会等待, 直到为 @domain 指定的同步域的所有异步函数调用都已完成.
*/
void async_synchronize_full_domain(struct async_domain *domain)
{
/*
* 此函数是下一层核心函数 async_synchronize_cookie_domain 的封装.
* 它直接调用后者, 并为其 cookie 参数传递 ASYNC_COOKIE_MAX.
* ASYNC_COOKIE_MAX 是一个宏, 代表了 cookie 值的上限.
* 使用这个值意味着"请等待所有已提交的、cookie值小于等于最大值的任务完成",
* 这实际上就等同于等待该域内所有已提交的任务完成.
*/
async_synchronize_cookie_domain(ASYNC_COOKIE_MAX, domain);
}
/*
* 将 async_synchronize_full_domain 函数导出, 供GPL许可的内核模块使用.
*/
EXPORT_SYMBOL_GPL(async_synchronize_full_domain);

这是异步同步机制的核心实现函数, 提供了最精细的控制. 它允许任务等待一个特定域(domain)中, 所有在某个时间点(cookie)之前提交的任务完成. cookie 就像一个序列号或检查点, 调用者可以获取一个当前的 cookie, 然后提交一系列异步任务, 最后使用这个 cookie 来等待这一批任务全部完成.

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
/**
* async_synchronize_cookie_domain - 在特定域内使用cookie检查点同步异步函数调用
* @cookie: 用作检查点的 async_cookie_t 值.
* @domain: 需要同步的域 (如果为 %NULL, 则同步所有已注册的域).
*
* 这个函数会等待, 直到为 @domain 指定的同步域中, 所有在 @cookie 之前提交的
* 异步函数调用都已完成.
*/
void async_synchronize_cookie_domain(async_cookie_t cookie, struct async_domain *domain)
{
/*
* 定义一个 ktime_t 类型的变量 starttime, 用于存储高精度时间戳.
*/
ktime_t starttime;

/*
* 使用 pr_debug 打印一条调试信息, 表明当前任务(由PID标识)开始等待.
* 这类信息仅在内核开启了动态调试时才会显示.
*/
pr_debug("async_waiting @ %i\n", task_pid_nr(current));
/*
* 调用 ktime_get() 获取当前时间, 记录等待开始的时刻, 用于性能调试.
*/
starttime = ktime_get();

/*
* 这是实现同步的核心. 调用 wait_event 宏使当前任务睡眠.
* async_done: 这是一个全局的等待队列头. 所有等待异步任务完成的进程都在此队列上睡眠.
* 当每个异步任务完成时(在 async_run_entry_fn 中), 都会调用 wake_up(&async_done)
* 来唤醒在这个队列上睡眠的任务.
* lowest_in_progress(domain) >= cookie: 这是睡眠的条件.
* lowest_in_progress(domain) 是一个函数(或宏), 它会扫描所有正在进行中的
* 异步任务(在指定的domain内), 并返回其中最小的那个 cookie 值.
* 只要还有一个正在进行的任务的 cookie 小于我们指定的检查点 cookie,
* 这个条件就为假, wait_event 会让当前任务继续睡眠.
* 直到所有小于检查点 cookie 的任务都完成了, lowest_in_progress 返回的值
* 才会大于或等于检查点 cookie, 条件变为真, wait_event 最终返回.
*/
wait_event(async_done, lowest_in_progress(domain) >= cookie);

/*
* 再次使用 pr_debug 打印调试信息, 报告当前任务已结束等待, 并打印出总的等待时间(微秒).
*/
pr_debug("async_continuing @ %i after %lli usec\n", task_pid_nr(current),
microseconds_since(starttime));
}
/*
* 将 async_synchronize_cookie_domain 函数导出, 供GPL许可的内核模块使用.
*/
EXPORT_SYMBOL_GPL(async_synchronize_cookie_domain);