3. 内核线程创建API:kthread_create()函数原型与参数、kthread_run()宏的封装、kthread_create_on_node()的NUMA感知
内核线程是Linux内核中执行特定内核任务的轻量级执行单元,它们运行在内核空间,拥有独立的task_struct,但共享内核地址空间。内核线程的创建主要通过一组精心设计的API完成,这些API提供了从基础创建到NUMA感知的完整能力。本节将深入剖析kthread_create()、kthread_run()宏以及kthread_create_on_node()的实现机制与使用场景。
3.1 kthread_create():内核线程创建的核心函数
kthread_create()是内核线程创建的基础API,它负责分配task_struct、初始化内核线程栈,并将线程置于暂停状态,等待调度器唤醒执行。其函数原型定义在include/linux/kthread.h中:
struct task_struct *kthread_create(int (*threadfn)(void *data),
void *data,
const char namefmt[], ...);
- threadfn:线程函数指针,当内核线程被唤醒并调度执行时,会调用此函数。函数签名必须为
int (*)(void *),返回值通常为0表示成功,负值表示错误。 - data:传递给线程函数的私有数据指针,允许线程函数访问调用者提供的上下文信息。
- namefmt:格式化字符串,用于设置内核线程的名称(即
task_struct->comm),支持可变参数,类似printk的格式化方式。
kthread_create() 后,新创建的线程处于 TASK_UNINTERRUPTIBLE 状态,不会立即执行。必须通过 wake_up_process() 显式唤醒,或使用 kthread_run() 宏一步完成创建与唤醒。
下表总结了kthread_create()各参数的类型与用途:
| 参数 | 类型 | 描述 |
|---|---|---|
| threadfn | int (*)(void *) |
线程入口函数,返回int类型 |
| data | void * |
传递给线程函数的参数指针 |
| namefmt | const char * |
线程名称的格式化字符串 |
| ... | 可变参数 | 用于填充namefmt中的格式化占位符 |
以下是一个典型的使用示例,创建一个名为"my_monitor"的内核线程:
struct task_struct *monitor_task;
monitor_task = kthread_create(monitor_function, NULL, "my_monitor/%d", cpu);
if (IS_ERR(monitor_task)) {
pr_err("Failed to create monitor thread: %ld\n", PTR_ERR(monitor_task));
return PTR_ERR(monitor_task);
}
wake_up_process(monitor_task); // 显式唤醒线程
kthread_create() 返回 ERR_PTR 编码的错误指针,必须使用 IS_ERR() 和 PTR_ERR() 进行判断,不能直接与NULL比较。
3.2 kthread_run():创建与唤醒的一步封装
kthread_run()是一个便捷宏,它将kthread_create()和wake_up_process()合并为一步操作,极大简化了内核线程的创建流程。其宏定义如下:
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k = kthread_create(threadfn, data, \
namefmt, ##__VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})
- 原子化操作:创建与唤醒合并,避免因代码疏忽导致线程永远不被唤醒的bug。
- 错误处理内置:仅在创建成功时调用
wake_up_process(),失败时直接返回错误指针。 - 返回值一致:返回
struct task_struct *,与kthread_create()完全兼容。
使用kthread_run()的典型代码片段:
struct task_struct *worker;
worker = kthread_run(worker_function, &worker_data, "kworker/%d", cpu_id);
if (IS_ERR(worker)) {
pr_err("Failed to start worker thread on CPU%d: %ld\n",
cpu_id, PTR_ERR(worker));
return PTR_ERR(worker);
}
// 此时 worker 线程已经在运行中
下表对比了kthread_create()与kthread_run()的使用差异:
| 特性 | kthread_create() | kthread_run() |
|---|---|---|
| 创建后状态 | TASK_UNINTERRUPTIBLE(暂停) | TASK_RUNNING(可运行) |
| 是否需要额外唤醒 | 是,需调用 wake_up_process() | 否,宏内部自动唤醒 |
| 代码行数 | 3行(创建+错误检查+唤醒) | 1行 |
| 适用场景 | 需要延迟启动或精细控制启动时机 | 绝大多数通用场景 |
kthread_run() 宏,它减少了出错概率,代码也更简洁。
3.3 kthread_create_on_node():NUMA感知的线程创建
在NUMA(Non-Uniform Memory Access)架构下,内存访问延迟取决于内存与CPU的物理距离。kthread_create_on_node()允许调用者指定线程的task_struct和内核栈应该分配在哪个NUMA节点上,从而优化内存访问局部性。其函数原型为:
struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
void *data,
int node,
const char namefmt[], ...);
- node:指定NUMA节点ID(从0开始),内核将优先从该节点分配
task_struct和内核栈内存。 - 其他参数与
kthread_create()完全一致。
NUMA感知创建的核心流程如下:
- 节点选择:调用者根据业务需求确定目标NUMA节点(通常绑定到特定CPU所在的节点)。
- 内存分配:通过
kmem_cache_alloc_node()在指定节点上分配task_struct。 - 栈分配:通过
__vmalloc_node_range()在指定节点上分配内核栈。 - 初始化:设置线程函数、数据指针和名称,线程仍处于暂停状态。
以下是一个NUMA感知的线程创建示例:
int target_node = cpu_to_node(cpu_id);
struct task_struct *numa_worker;
numa_worker = kthread_create_on_node(numa_worker_fn, &data,
target_node,
"numa_worker/%d", cpu_id);
if (IS_ERR(numa_worker)) {
pr_err("Failed to create NUMA-aware worker on node %d\n", target_node);
return PTR_ERR(numa_worker);
}
wake_up_process(numa_worker);
- 如果指定节点内存不足,内核会回退到其他节点分配,不会导致创建失败。
- 线程创建后的调度行为由调度器决定,
kthread_create_on_node()仅保证初始内存分配的节点亲和性。 - 对于长时间运行的内核线程,建议配合
kthread_bind()将线程绑定到特定CPU,以最大化NUMA局部性收益。
3.4 三种API的选型对比
在实际开发中,选择合适的创建API取决于具体场景。下表从多个维度对比了三种API的适用性:
| 选型维度 | kthread_create() | kthread_run() | kthread_create_on_node() |
|---|---|---|---|
| 代码简洁性 | 中等 | 高 | 中等 |
| 启动控制 | 精细控制 | 自动启动 | 精细控制 |
| NUMA感知 | 无 | 无 | 支持 |
| 典型场景 | 需要修改线程优先级/调度策略后再启动 | 通用内核线程创建 | 高性能计算、数据库、网络栈等NUMA敏感场景 |
- 通用场景:优先使用
kthread_run(),简洁且安全。 - 需要延迟启动或修改线程属性:使用
kthread_create()+wake_up_process()。 - NUMA架构下的性能敏感型线程:使用
kthread_create_on_node(),并配合kthread_bind()绑定到目标CPU。
通过深入理解这三个API的设计哲学与实现细节,开发者可以根据实际需求选择最合适的线程创建方式,在代码简洁性、控制灵活性和性能优化之间取得平衡。