[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_t
和async_synchronize_cookie()
/async_synchronize_full()
。这使得调用者可以在一个稍后的、合适的“检查点”等待一个或所有已提交的异步任务完成。
目前该技术的社区活跃度和主流应用情况如何?
async
是一个非常稳定、成熟,但相对小众的内核组件。
- 社区活跃度:其代码库非常稳定,几乎没有改动。它被认为是“已完成”的功能,维护工作仅限于必要的bug修复。
- 主流应用:
- 内核启动:这是它最重要和最著名的应用场景。在
do_initcalls()
中,内核会使用异步模式来并行执行模块初始化。 - 设备探测:在一些复杂的子系统(如
component
框架)或驱动中,可能会用它来异步执行一些探测或绑定的收尾工作。 - 总体而言,它的使用场景远不如工作队列(workqueues)广泛,主要局限于那些需要批量并行化、且后续有明确同步点的流程。
- 内核启动:这是它最重要和最著名的应用场景。在
核心原理与设计
它的核心工作原理是什么?
async
的核心原理是作为内核全局工作队列的一个极简主义前端封装。
- 数据结构:核心是一个小的内部控制结构
async_entry
。它包含了要执行的函数指针、传递给该函数的参数、一个用于同步的completion
对象,以及一个work_struct
。 - 调度 (
async_schedule
):当内核代码调用async_schedule(func, data)
时,会发生以下事情:- 从一个预先分配的slab缓存中获取一个
async_entry
对象。 - 将传入的
func
和data
存入这个对象。 - 初始化该对象中的
completion
和work_struct
。 - 最关键的一步:调用
schedule_work()
,将这个async_entry
中的work_struct
放入内核的全局系统工作队列(system_wq
)中。 - 函数返回一个
async_cookie_t
,它实际上就是这个async_entry
的地址。
- 从一个预先分配的slab缓存中获取一个
- 执行:稍后,一个内核的worker线程会从
system_wq
中取出这个work_struct
并执行其处理函数。这个处理函数(async_run_entry
)的作用很简单:- 从
work_struct
中找到async_entry
的地址。 - 调用
async_entry
中存储的func
指针,并传入data
参数。 - 函数执行完毕后,调用
complete()
来唤醒任何可能在等待这个任务完成的代码。
- 从
- 同步:
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 | // 定义内核异步功能初始化函数 |
async_run_entry_fn: 执行并清理异步任务
此函数是内核异步工作队列 (workqueue
) 的实际执行体. 当内核的工作线程 (kworker
) 从异步工作队列 async_wq
中取出一个待处理的工作项 (work_struct
) 时, 就会调用这个函数. 它的核心职责是: 执行用户最初请求异步调用的函数, 然后清理与该任务相关的所有资源, 并通知其他可能正在等待的内核部分.
1 | /* |
__async_schedule_node_domain: 异步任务调度的核心实现
此函数是Linux内核异步执行框架的内部核心, 负责将一个已经准备好的异步任务实体(entry
)加入到相应的等待队列, 并最终提交给内核工作队列(workqueue
)去执行. 它是 async_schedule_node_domain
函数在成功分配了任务实体后的实际执行者.
1 | /* |
async_schedule_node_domain: 在指定的NUMA节点上使用特定域调度一个异步函数执行
此函数是内核异步调度框架的核心实现. 它的主要作用是接收一个函数指针(func
)和其参数(data
), 并将它们打包成一个异步工作项, 然后提交给内核的工作队列, 以便稍后由内核线程执行. 它允许指定一个同步域(domain
), 用于后续更细粒度的同步等待.
1 | /** |
async_schedule_node: 在指定的NUMA节点上使用默认域调度异步函数
此函数是 async_schedule_node_domain
的一个简化版本. 它隐藏了 domain
(同步域) 的概念, 总是使用系统默认的同步域 (async_dfl_domain
). 这为那些不需要复杂同步控制的调用者提供了便利.
1 | /** |
async_schedule_dev: 在与设备关联的NUMA节点上调度异步函数
这是一个静态内联函数, 提供了更高层次的封装, 主要面向设备驱动程序的开发者. 它使得为特定设备调度一个异步任务变得非常简单, 因为它会自动从设备结构体 (struct device
) 中推断出 NUMA 节点信息, 并将设备指针自身作为参数传递给异步执行的函数.
1 | /** |
lowest_in_progress: 查找进行中任务的最低Cookie值
此函数是内核异步同步机制 (async_synchronize_*
) 的核心辅助函数, 它的唯一作用是查找并返回当前所有正在排队或执行的异步任务中, 拥有最小 cookie
值的那个 cookie
.
它的工作原理基于一个简单而高效的设计:
- 所有新提交的异步任务, 都会被添加到相应待处理链表 (
pending
或global_pending
) 的尾部. cookie
值是单调递增的.- 因此, 待处理链表的头部的那个任务, 必然是所有待处理任务中最早被提交的, 也就拥有最低的
cookie
值.
1 | /* |
async_synchronize_full: 同步(等待)所有异步函数调用完成
此函数是内核异步框架中最强力的一个全局同步点. 调用此函数的任务将会进入睡眠, 直到系统中所有被调度执行的、尚未完成的异步任务全部执行完毕. 它是一个全局屏障, 确保在它返回后, 整个异步子系统处于静默状态.
1 | /** |
async_synchronize_full_domain: 同步特定域内的所有异步函数调用
这是一个更具针对性的同步函数. 它允许调用者只等待某个特定”同步域” (domain
) 内的所有异步任务完成, 而不是等待全局所有的任务. 这在大型子系统中很有用, 这些子系统可能定义了自己的域, 以便能独立地管理和同步自己的后台任务, 而不受其他子系统的影响.
1 | /** |
async_synchronize_cookie_domain: 使用cookie检查点同步特定域内的异步函数
这是异步同步机制的核心实现函数, 提供了最精细的控制. 它允许任务等待一个特定域(domain
)中, 所有在某个时间点(cookie
)之前提交的任务完成. cookie
就像一个序列号或检查点, 调用者可以获取一个当前的 cookie
, 然后提交一系列异步任务, 最后使用这个 cookie
来等待这一批任务全部完成.
1 | /** |