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比较。
kthread_create() 内核线程创建流程 调用者进程 kthread_create() 分配 task_struct 初始化内核栈 设置线程名称 返回 task_struct 线程处于 TASK_UNINTERRUPTIBLE 不会立即执行 需调用 wake_up_process() 显式唤醒

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[], ...);
NUMA感知的关键参数:
  • node:指定NUMA节点ID(从0开始),内核将优先从该节点分配 task_struct 和内核栈内存。
  • 其他参数与 kthread_create() 完全一致。

NUMA感知创建的核心流程如下:

  1. 节点选择:调用者根据业务需求确定目标NUMA节点(通常绑定到特定CPU所在的节点)。
  2. 内存分配:通过 kmem_cache_alloc_node() 在指定节点上分配 task_struct。
  3. 栈分配:通过 __vmalloc_node_range() 在指定节点上分配内核栈。
  4. 初始化:设置线程函数、数据指针和名称,线程仍处于暂停状态。

以下是一个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);
NUMA感知的注意事项:
  • 如果指定节点内存不足,内核会回退到其他节点分配,不会导致创建失败。
  • 线程创建后的调度行为由调度器决定,kthread_create_on_node() 仅保证初始内存分配的节点亲和性。
  • 对于长时间运行的内核线程,建议配合 kthread_bind() 将线程绑定到特定CPU,以最大化NUMA局部性收益。
kthread_create_on_node() NUMA感知架构 NUMA Node 0 CPU 0 本地内存 task_struct 内核栈 NUMA Node 1 CPU 1 本地内存 跨节点访问 高延迟 kthread_create_on_node() 指定 node 参数 选择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的设计哲学与实现细节,开发者可以根据实际需求选择最合适的线程创建方式,在代码简洁性、控制灵活性和性能优化之间取得平衡。