Linux 内核 padata 并行执行机制深度解析:串行化作业与多线程作业的 API 实战指南
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
导读
本文基于 Linux 内核官方文档 Documentation/core-api/padata.rst,结合 include/linux/padata.h、kernel/padata.c 以及其唯一生产级消费者 crypto/pcrypt.c 的源码实现,全面讲解 padata 并行执行机制的两种作业模式:串行化作业(Serialized Jobs)——把大量独立任务分散到多 CPU 上并行执行、同时严格保持提交顺序;以及多线程作业(Multithreaded Jobs)——把单个大任务按 chunk 切分给主线程与若干辅助线程协同完成。读完本文,你将掌握从实例初始化、CPU 掩码调整、作业提交/序列化/销毁的完整 API 用法,并理解其背后的重排队列、RCU 换壳与 CPU 热插拔处理等底层原理。
padata 是什么:为"并行 + 保序"而生的内核通用机制
padata 是内核提供的一种将任务分发到多个 CPU 上并行执行、同时可选地保持任务完成顺序的机制。它的设计初衷来自 IPsec:IPsec 需要对海量数据包进行加解密,但又不能打乱数据包的顺序。当前内核中,串行化作业(serialized job)的唯一使用者正是 IPsec 栈(经由 pcrypt 并行密码封装层),而 padata 同时支持多线程作业,可把一个大任务均匀切分、在线程间负载均衡并协调收尾。
从源码结构看,padata 的完整实现位于 kernel/padata.c,对外接口声明于 include/linux/padata.h,相关文档被收录进 Documentation/core-api/index.rst。整个子系统围绕几个核心数据结构展开:
struct padata_instance——实例级总控结构,持有并行/串行两套工作队列、用户提交的 CPU 掩码、挂载的 shell 链表与 sysfs kobject(见 include/linux/padata.h#L162-L175);struct padata_shell——提交作业的入口壳,通过 RCU 指向内部struct parallel_data,从而支持"运行中动态更换控制结构"(见 include/linux/padata.h#L115-L120);struct parallel_data——依赖 CPU 掩码的内控结构,包含 per-CPU 的 reorder 链表与串行队列(见 include/linux/padata.h#L94-L103);struct padata_priv——单个作业的表示,内嵌parallel()与serial()两个回调(见 include/linux/padata.h#L36-L44);struct padata_mt_job——多线程作业的描述结构(见 include/linux/padata.h#L138-L147)。
构建配置:CONFIG_PADATA 与它的唯一自动选择者
padata 本体是一个纯布尔配置项,定义于 init/Kconfig#L2332-L2334:
config PADATA depends on SMP bool它依赖SMP(对称多处理)——padata 的价值完全建立在多 CPU 并行之上。目前内核中没有直接暴露给用户手动开关的界面,而是被密码学子系统中的并行封装模板CRYPTO_PCRYPT自动选择。参见 crypto/Kconfig#L204-L212:
config CRYPTO_PCRYPT tristate "Parallel crypto engine" depends on SMP select PADATA select CRYPTO_MANAGER select CRYPTO_AEAD help This converts an arbitrary crypto algorithm into a parallel algorithm that executes in kernel threads.也就是说,当用户启用CONFIG_CRYPTO_PCRYPT时,CONFIG_PADATA会被自动拉取,从而在编译进内核时执行kernel/padata.c中的padata_init()(见 kernel/padata.c#L1089-L1118),完成两件启动工作:
- 注册 CPU 热插拔多实例回调(
CPUHP_AP_ONLINE_DYN动态状态,名称为"padata:online"),用于在 CPU 上线/下线时自动重建控制结构; - 按
num_possible_cpus()预分配一个全局struct padata_work池,挂在padata_free_works空闲链表上,供作业提交时复用。
运行串行化作业(Running Serialized Jobs)
串行化作业模式面向"大量独立小作业、但要求按提交顺序完成回调"的场景,是 padata 最经典也最完整的用法。
第一步:初始化实例与 shell
使用 padata 的第一步是分配一个padata_instance,用于总体控制作业如何运行:
#include <linux/padata.h> struct padata_instance *padata_alloc(const char *name);name仅用于标识实例。从实现看(kernel/padata.c#L949-L1014),padata_alloc()会:
- 创建两个专用工作队列:并行工作队列
"%s_parallel"(WQ_UNBOUND,即不绑定特定 CPU),串行工作队列"%s_serial"(WQ_MEM_RECLAIM | WQ_CPU_INTENSIVE | WQ_PERCPU,max_active=1,保证同一 CPU 上串行回调逐条执行); - 把用户提交的并行/串行 CPU 掩码初始化为
cpu_possible_mask(所有可能的 CPU); - 初始化 kobject、互斥锁,并在
CONFIG_HOTPLUG_CPU下把实例挂入热插拔回调链。
接着分配一个padata_shell完成初始化:
struct padata_shell *padata_alloc_shell(struct padata_instance *pinst);一个padata_shell用于向 padata 提交作业,并允许一组作业被独立地串行化。一个实例可以关联一个或多个 shell,每个 shell 各自维护一条独立的作业序列(kernel/padata.c#L1035-L1064)。shell 的关键设计是它内部的parallel_data指针用 RCU 保护,因此当 CPU 掩码或 CPU 热插拔事件导致底层控制结构需要替换时,可以"在线换壳"而不阻塞正在执行的作业。
实际内核中 shell 与实例的对应关系,可以在 pcrypt 中看到最典型的例子(crypto/pcrypt.c#L260-L265):pcrypt 为每个 AEAD 算法实例(pcrypt_create_aead)同时分配psenc、psdec两个 shell——一个用于加密、一个用于解密,而它们共享同一个 padata 实例。
第二步:修改 CPU 掩码
运行作业所用的 CPU 可以通过两种方式修改:编程方式调用padata_set_cpumask(),或者通过 sysfs。
int padata_set_cpumask(struct padata_instance *pinst, int cpumask_type, cpumask_var_t cpumask);其中cpumask_type取值为 include/linux/padata.h#L22-L23 定义的两个常量:
| 常量 | 含义 |
|---|---|
PADATA_CPU_PARALLEL(0x02) | 并行掩码,指定哪些处理器会被用来并行执行提交给该实例的作业 |
PADATA_CPU_SERIAL(0x01) | 串行掩码,指定哪些处理器允许被用作序列化回调处理器 |
cpumask即要使用的新掩码。实现上(kernel/padata.c#L716-L745),该函数在cpus_read_lock()+ 实例互斥锁的保护下,根据类型合并出新的并行/串行掩码对,然后走__padata_set_cpumasks()的公共路径:先做合法性校验,再调用padata_setup_cpumasks()通过apply_workqueue_attrs()把并行工作队列的 worker 限制到新的并行掩码(见 kernel/padata.c#L387-L401),最后执行padata_replace()用 RCU 原子替换所有 shell 的parallel_data。
对于有 sysfs 支持的实例,用户可以在/sys/kernel/<实例名>/下看到两个文件parallel_cpumask与serial_cpumask(属性定义于 kernel/padata.c#L887-L888,读写回调实现于 kernel/padata.c#L835-L878,0644权限)。例如 pcrypt 的实例位于/sys/kernel/pcrypt/<实例名>,把加密实例的并行掩码设为f(二进制 1111,即 CPU 0-3):
echo f > /sys/kernel/pcrypt/pencrypt/parallel_cpumask写入的字符串经bitmap_parse()解析为位图后,会最终调用到padata_set_cpumask(),与编程方式完全等价。读取这些文件显示的是用户提交的掩码,它可能与实际"可用掩码"不同——这一点正是下一节的核心。
用户掩码与可用掩码:为什么可以包含离线 CPU
padata 内部维护着两对CPU 掩码(每对含一个并行掩码和一个串行掩码):
- 用户提交掩码(user-supplied):实例分配时默认取所有可能的 CPU,之后可按上述两种方式修改;
- 可用掩码(usable):始终是用户提交掩码的子集,只包含其中的在线 CPU——这才是 padata 实际使用的掩码。
两者的关系在padata_alloc_pd()中一目了然(kernel/padata.c#L563-L564):
cpumask_and(pd->cpumask.pcpu, pinst->cpumask.pcpu, cpu_online_mask); cpumask_and(pd->cpumask.cbcpu, pinst->cpumask.cbcpu, cpu_online_mask);因此,向 padata 提交一个包含离线 CPU的掩码是完全合法的:离线 CPU 暂时不会被使用,而一旦它上线,padata 会自动把它纳入可用范围(由padata_cpu_online()热插拔回调触发,见 kernel/padata.c#L756-L775)。反之,如果一个 CPU 下线,padata_cpu_offline()(kernel/padata.c#L777-L796)会重建控制结构并剔除该 CPU;若校验发现并行或串行掩码与在线集合没有任何交集,则把实例标记为PADATA_INVALID并停止(__padata_stop()),对应实现padata_validate_cpumask()(kernel/padata.c#L653-L675)。
需要特别提醒:修改 CPU 掩码是昂贵的操作——它涉及工作队列属性重建、为所有 shell 分配新的parallel_data、RCU 同步等待旧结构退役等一连串动作,因此不应该频繁调用。
第三步:提交一个作业
真正向 padata 实例提交工作,需要创建struct padata_priv结构来表示一个作业:
struct padata_priv { /* Other stuff here... */ void (*parallel)(struct padata_priv *padata); void (*serial)(struct padata_priv *padata); };这个结构几乎总是被嵌入某个更大的、与具体工作相关的结构中。它的大部分字段对 padata 是私有的,但使用者需要在初始化时将结构清零,并提供parallel()与serial()两个回调函数。完整的字段布局(include/linux/padata.h#L36-L44)还包含:链表节点list、内控结构指针pd、回调 CPUcb_cpu、序列号seq_nr(由 padata 分配)、以及用于把并行阶段结果传递给串行回调的info字段。
作业提交通过如下函数完成:
int padata_do_parallel(struct padata_shell *ps, struct padata_priv *padata, int *cb_cpu);ps与padata必须按上述方式设置好;cb_cpu指向作业完成时最终回调(即serial())期望运行的首选 CPU,它必须在当前实例的串行掩码中——如果不在,cb_cpu指向的值会被就地更新为 padata 实际选中的 CPU(kernel/padata.c#L196-L203)。
返回值语义(对应实现 kernel/padata.c#L180-L235):
| 返回值 | 含义 |
|---|---|
0 | 成功,作业已进入执行流程 |
-EBUSY | 有其他人正在修改该实例的 CPU 掩码(实例处于PADATA_RESET状态,旧的parallel_data正在被替换) |
-EINVAL | cb_cpu不在串行掩码中、并行/串行掩码中无在线 CPU、或实例已停止/无效(PADATA_INIT未置位或PADATA_INVALID被置位) |
每个提交给padata_do_parallel()的作业,最终会在某个 CPU 上被恰好一次调用其parallel()函数,因此真正的并行度来自"一次提交多个作业"。parallel()在软件中断(BH)被禁用的上下文中运行,因此它不能睡眠。它只接收padata_priv结构指针这一个参数,关于实际工作的信息通常需要用container_of()从内嵌它的外层结构中取回。
parallel()没有返回值:padata 假定从此刻起parallel()已接管该作业。作业不必在本次调用内完成,但如果parallel()留下了未完成的工作,它必须准备好在前一个作业完成之前被再次调用以承接新作业——这与串行化作业模式下"提交-完成回调"解耦的异步模型保持一致。
pcrypt 如何落实这两个回调
以 pcrypt 的加密路径为例(crypto/pcrypt.c#L80-L131):
pcrypt_aead_encrypt()把struct padata_priv清零后,设置padata->parallel = pcrypt_aead_enc、padata->serial = pcrypt_aead_serial,随后调用padata_do_parallel(ictx->psenc, padata, &ctx->cb_cpu);parallel回调pcrypt_aead_enc()实际调用底层crypto_aead_encrypt(req),若结果为-EINPROGRESS/-EBUSY(异步算法尚未完成)则直接返回,否则把结果存入padata->info并调用padata_do_serial(padata);- 异步完成路径
pcrypt_aead_done()同样在完成后设置padata->info = err并调用padata_do_serial(); - 当
padata_do_parallel()返回-EBUSY时,pcrypt 会退化为非并行直通模式,直接对底层算法加密,保证 CPU 掩码调整期间请求不丢。
这段代码同时印证了文档强调的两个要点:parallel()中作业可以异步挂起(-EINPROGRESS时不立即调用padata_do_serial()),以及info字段正是"从并行阶段向串行阶段传结果"的官方通道。
第四步:序列化作业
当一个作业完成时,parallel()(或任何真正完成工作的函数)必须调用下面的函数通知 padata:
void padata_do_serial(struct padata_priv *padata);在未来的某个时刻,padata_do_serial()会触发对padata_priv结构中serial()函数的调用。该调用发生在最初padata_do_parallel()请求的 CPU 上,同样以本地软件中断禁用的状态运行。注意这个调用可能被推迟一段时间——因为 padata 会不遗余力地确保作业按照提交的顺序完成(即按序列号严格重排)。
重排与保序的底层实现
padata_do_serial()的实现(kernel/padata.c#L359-L385)揭示了保序的核心机制:
- 通过
padata_cpu_hash(pd, seq_nr)(kernel/padata.c#L66-L75)把作业按seq_nr % 并行掩码CPU数散列到某个 CPU 的reorder 链表上; - 在链表上按序列号升序插入(比较时使用有符号差值,以正确处理 32 位序列号回绕);
- 只有当该作业的
seq_nr恰好等于pd->processed(下一个应完成的序号)时,才调用padata_reorder()。
padata_reorder()(kernel/padata.c#L280-L316)随后会连续"放行"一连串序号连续的作业:每放行一个,processed加一,并用cpumask_next_wrap()遍历并行掩码中的 CPU(序列号回绕到 0 时重置为首个 CPU);被放行的作业被追加到其cb_cpu对应的 per-CPU 串行队列(squeue),并通过queue_work_on(cb_cpu, serial_wq, &squeue->work)调度。padata_serial_worker()(kernel/padata.c#L318-L349)在 BH 关闭状态下批量取出该队列,逐个调用padata->serial(padata),最后按处理数量归还parallel_data的引用计数。这也解释了为什么乱序到达的作业必须等待:如果下一个序号的作业还在别的 CPU 上并行处理、尚未进入 reorder 队列,padata_find_next()(kernel/padata.c#L248-L278)会返回 NULL 从而结束本轮放行,把"等待状态"记录下来,待该作业稍后到达时再继续。
第五步:销毁
清理一个 padata 实例,自然是对应分配函数的逆序调用:
void padata_free_shell(struct padata_shell *ps); void padata_free(struct padata_instance *pinst);padata_free_shell()(kernel/padata.c#L1072-L1087)把 shell 从实例的pslist摘除并释放其parallel_data引用;padata_free()(kernel/padata.c#L1022-L1026)本质是kobject_put(),当 kobject 引用计数归零时,由padata_sysfs_release()触发__padata_free()(kernel/padata.c#L801-L815):注销热插拔节点、WARN_ON(!list_empty(&pinst->pslist))检查是否还有未释放的 shell、释放掩码变量并销毁两条工作队列。
用户有责任确保在调用上述任一函数之前,所有未完成的作业都已完成——否则释放后继续到达的padata_do_serial()将访问已释放的内存。
运行多线程作业(Running Multithreaded Jobs)
多线程作业面向"单个大任务"的场景:一个作业有一个主线程和零个或多个辅助线程,主线程既参与作业、又等待所有辅助线程结束。padata 把作业切分成若干chunk——一个 chunk 是一个线程在一次线程函数调用中完成的那部分工作。
运行一个多线程作业需要做三件事:
描述作业:定义
struct padata_mt_job结构,包含线程函数指针以及任务范围、对齐、最小 chunk、最大线程数等参数(见 include/linux/padata.h#L138-L147);字段 说明 thread_fnpadata 每次给线程分配一个 chunk 时调用的线程函数 fn_arg传给线程函数的参数指针 start作业起点(单位由作业自定义) size作业大小(单位由作业自定义) align传给线程函数的范围落在该边界上(作业首尾可能例外) min_chunk最小 chunk 大小,向 padata 表达"一个 worker 一次至少做多少工作才合适" max_threads作业使用的最大线程数,实际数量可能因任务大小与最小 chunk 而更少 numa_aware为 true 时按轮转方式把作业分发到不同 NUMA 节点上有 CPU 的位置 定义线程函数:接受三个参数
start、end、arg,前两个界定线程操作的区间,最后一个指向作业的共享状态(通常分配在主线程栈上);调用入口:调用
padata_do_multithreaded(),该函数在作业完成后返回:
void padata_do_multithreaded(struct padata_mt_job *job);padata_do_multithreaded()的实现(kernel/padata.c#L444-L507)值得细读:
- 线程数确定:
nworks = max(size / max(min_chunk, align), 1)再与max_threads取小;若nworks == 1,直接在当前线程串行执行thread_fn(start, start + size, arg),免去一切协调开销; - chunk 大小:
ps.chunk_size = size / (nworks * load_balance_factor),其中load_balance_factor = 4(静态常量,见 kernel/padata.c#L447),即把作业切成"线程数 × 4"份以改善负载均衡;再取max(chunk_size, min_chunk)、至少为 1(防止除零),最后按align向上取整; - 执行模型:辅助线程从全局空闲的
padata_work池中取工作项(padata_work_alloc_mt()从下标 1 开始分配,因为下标 0 即当前任务自己参与作业),通过queue_work()或(numa_aware时)queue_work_node()投递到系统默认工作队列;主线程则用PADATA_WORK_ONSTACK标志在栈上初始化一个工作项并直接参与执行(省去一次启动 worker 的开销); - 协调:
padata_mt_helper()(kernel/padata.c#L404-L436)是每个线程共同的工作循环——在自旋锁保护下抢占下一个 chunk(start = job->start; size = roundup(start+1, chunk_size) - start,保证 chunk 对齐;end = start + size),更新作业游标后释放锁再调用thread_fn(start, end, fn_arg),如此循环直至job->size归零;每个线程完成后nworks_fini加一,最后一个完成的线程通过complete(&ps.completion)唤醒主线程; - 收尾:主线程
wait_for_completion()等待全部辅助线程结束,销毁栈上工作项并把辅助工作项归还空闲池。
需要说明:padata_do_multithreaded()在头文件中被标记为__init(见 include/linux/padata.h#L186),即仅限内核初始化阶段使用;当CONFIG_PADATA未启用时,头文件提供了内联退化实现(include/linux/padata.h#L190-L194),直接单线程执行整个范围,保证调用方代码在两种配置下都能编译运行。
接口总览(Interface)
padata 的完整 API 契约以 kernel-doc 注释的形式内联在两个源文件中,文档通过 kernel-doc 指令直接引用,读者可以随时查阅权威注释:
- include/linux/padata.h:全部核心数据结构的字段语义(
padata_priv、padata_shell、parallel_data、padata_mt_job、padata_instance)及函数原型; - kernel/padata.c:每个导出函数的完整行为说明、参数约束与返回值(如
padata_do_parallel的-EINVAL/-EBUSY语义、padata_set_cpumask的掩码类型约束等)。
所有对外接口(include/linux/padata.h#L177-L195)汇总如下:
| 函数 | 作用 |
|---|---|
padata_init() | 内核启动时初始化空闲工作池与 CPU 热插拔回调 |
padata_alloc(name) | 分配并初始化实例 |
padata_free(pinst) | 释放实例 |
padata_alloc_shell(pinst) | 分配 shell,用于提交作业 |
padata_free_shell(ps) | 释放 shell |
padata_do_parallel(ps, padata, cb_cpu) | 提交串行化作业 |
padata_do_serial(padata) | 通知作业完成,触发保序的串行回调 |
padata_do_multithreaded(job) | 运行多线程作业(__init阶段专用) |
padata_set_cpumask(pinst, type, cpumask) | 编程方式修改并行/串行 CPU 掩码 |
实例解读:pcrypt 如何组织 padata
作为 padata 串行化作业唯一的实际使用者,crypto/pcrypt.c 提供了教科书级的完整生命周期示范:
- 启动(crypto/pcrypt.c#L352-L376):创建
pcryptkset,随后分配两个实例——pencrypt(加密)与pdecrypt(解密),各自通过pcrypt_sysfs_add()挂到/sys/kernel/pcrypt/<实例名>; - 每个算法实例(
pcrypt_create_aead,crypto/pcrypt.c#L244-L301):从共享的 padata 实例上再分配加密/解密两个 shell,并把pcrypt_aead_encrypt/decrypt接到 AEAD 模板的encrypt/decrypt钩子上; - 回调 CPU 的选择(
pcrypt_aead_init_tfm,crypto/pcrypt.c#L186-L209):每个tfm初始化时按atomic_inc_return(&tfm_count) % online_cpus轮转挑选一个在线 CPU 作为cb_cpu,实现回调负载在串行掩码上的均衡分布; - sysfs 运行时调优:管理员可以通过
echo修改parallel_cpumask/serial_cpumask,例如把加密并行作业限制到前 4 个 CPU;读取时看到的是用户提交的掩码(可能与可用掩码不同)。
这一实例同时说明了文档中的一个重要设计:一个 padata_instance 承载多路相互独立的作业序列(加密流与解密流互不干扰,各自保序),其实现基础正是"一个实例可挂多个 shell,每个 shell 独立串行化"。
小结
padata 是 Linux 内核中"并行执行 + 可选保序"的通用基础设施:
- 串行化作业以
padata_instance/padata_shell/padata_priv三层结构组织,通过 per-CPU reorder 链表 + 序列号严格重排,在多个 CPU 上并行处理的同时保证serial()回调按提交顺序执行——该能力当前唯一的生产消费者是 IPsec 的 pcrypt 并行密码封装; - 多线程作业通过
padata_mt_job描述任务,按"线程数 × 4"的动态 chunk 切分实现负载均衡,支持对齐、最小 chunk、最大线程数与 NUMA 感知分发,主线程亲自参与执行并等待辅助线程收尾; - CPU 掩码管理(编程式
padata_set_cpumask()与 sysfs 双通道)配合"用户掩码/可用掩码"分层与 CPU 热插拔回调,让实例能够在 CPU 上下线时自动调整而不中断服务——但掩码修改是昂贵操作,应避免高频调用; - 所有接口的权威语义以 kernel-doc 形式内联在 include/linux/padata.h 与 kernel/padata.c 中,任何内核模块开发者都可以把 padata 引入自己的子系统,以获取与 IPsec 同款的多核并行与严格保序能力。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考