Linux 内核 padata 并行执行机制深度解析:串行化作业与多线程作业的 API 实战指南
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 上并行执行、同时可选地保持任务完成顺序的机制。它的设计初衷来自 IPsecIPsec 需要对海量数据包进行加解密但又不能打乱数据包的顺序。当前内核中串行化作业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-L175struct padata_shell——提交作业的入口壳通过 RCU 指向内部struct parallel_data从而支持运行中动态更换控制结构见 include/linux/padata.h#L115-L120struct parallel_data——依赖 CPU 掩码的内控结构包含 per-CPU 的 reorder 链表与串行队列见 include/linux/padata.h#L94-L103struct padata_priv——单个作业的表示内嵌parallel()与serial()两个回调见 include/linux/padata.h#L36-L44struct padata_mt_job——多线程作业的描述结构见 include/linux/padata.h#L138-L147。构建配置CONFIG_PADATA 与它的唯一自动选择者padata 本体是一个纯布尔配置项定义于 init/Kconfig#L2332-L2334config PADATA depends on SMP bool它依赖SMP对称多处理——padata 的价值完全建立在多 CPU 并行之上。目前内核中没有直接暴露给用户手动开关的界面而是被密码学子系统中的并行封装模板CRYPTO_PCRYPT自动选择。参见 crypto/Kconfig#L204-L212config 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-L1014padata_alloc()会创建两个专用工作队列并行工作队列%s_parallelWQ_UNBOUND即不绑定特定 CPU串行工作队列%s_serialWQ_MEM_RECLAIM | WQ_CPU_INTENSIVE | WQ_PERCPUmax_active1保证同一 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-L265pcrypt 为每个 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_PARALLEL0x02并行掩码指定哪些处理器会被用来并行执行提交给该实例的作业PADATA_CPU_SERIAL0x01串行掩码指定哪些处理器允许被用作序列化回调处理器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-L8780644权限。例如 pcrypt 的实例位于/sys/kernel/pcrypt/实例名把加密实例的并行掩码设为f二进制 1111即 CPU 0-3echo f /sys/kernel/pcrypt/pencrypt/parallel_cpumask写入的字符串经bitmap_parse()解析为位图后会最终调用到padata_set_cpumask()与编程方式完全等价。读取这些文件显示的是用户提交的掩码它可能与实际可用掩码不同——这一点正是下一节的核心。用户掩码与可用掩码为什么可以包含离线 CPUpadata 内部维护着两对CPU 掩码每对含一个并行掩码和一个串行掩码用户提交掩码user-supplied实例分配时默认取所有可能的 CPU之后可按上述两种方式修改可用掩码usable始终是用户提交掩码的子集只包含其中的在线 CPU——这才是 padata 实际使用的掩码。两者的关系在padata_alloc_pd()中一目了然kernel/padata.c#L563-L564cpumask_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 实际选中的 CPUkernel/padata.c#L196-L203。返回值语义对应实现 kernel/padata.c#L180-L235返回值含义0成功作业已进入执行流程-EBUSY有其他人正在修改该实例的 CPU 掩码实例处于PADATA_RESET状态旧的parallel_data正在被替换-EINVALcb_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-L131pcrypt_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()或任何真正完成工作的函数必须调用下面的函数通知 padatavoid 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是每个线程共同的工作循环——在自旋锁保护下抢占下一个 chunkstart job-start; size roundup(start1, 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直接单线程执行整个范围保证调用方代码在两种配置下都能编译运行。接口总览Interfacepadata 的完整 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)释放 shellpadata_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_aeadcrypto/pcrypt.c#L244-L301从共享的 padata 实例上再分配加密/解密两个 shell并把pcrypt_aead_encrypt/decrypt接到 AEAD 模板的encrypt/decrypt钩子上回调 CPU 的选择pcrypt_aead_init_tfmcrypto/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),仅供参考