Linux 内核线程的作用与分类

7次阅读

内核线程是内核自行创建、仅运行于内核态的轻量级执行流,如kswapd0、kthreadd,mm为NULL、无用户空间上下文、不可被信号中断;主要分周期轮询(如内存回收)和事件唤醒(如日志提交)两类,通过kthread_run()创建,不共享用户地址空间且不参与cgroup资源统计。

Linux 内核线程的作用与分类

内核线程是啥?它真在“后台默默干活”

内核线程不是用户能直接 fork()pthread_create() 出来的那种线程,它是内核自己启动、只运行在内核态的执行流,比如 kthreaddkswapd0khungtaskd。它们不走用户空间,不加载 ELF,也不用切换用户页表(mm 字段为 NULL),所以开销极小,且不会被信号中断或被用户态调度策略干扰。

你用 ps -Al | grep '^k.*d$' 看到的那些名字以 k 开头、以 d 结尾的进程,基本都是内核线程——它们不是“守护进程”的用户态模拟,而是内核原生的调度实体。

两类典型用途:周期轮询 vs 事件唤醒

内核线程主要就干这两类活,别看简单,但设计意图非常明确:

  • 周期性服务型:比如 kswapd0 每隔几十毫秒扫描内存水位,一旦低于 pages_low 就开始回收页;pdflush(旧内核)或 writeback 线程定期把脏页刷回块设备。这类线程通常用 schedule_timeout()msleep_interruptible() 控制节奏,避免空转耗 CPU。
  • 事件驱动型:比如 kthreadd 是所有内核线程的“父进程”,它本身不干活,只等其他子线程通过 kthread_create_on_node() 提交任务后,再调用 wake_up_process() 唤醒对应线程;又如文件系统日志线程(jbd2/sda1-8)只在事务提交队列非空时才被唤醒。

关键区别在于:前者主动睡、主动醒;后者全程休眠,靠 wait_event_*() 等待显式唤醒。写错唤醒逻辑,线程就永远卡住——没有用户态 SIGKILL 能杀掉它。

和用户线程、轻量级进程(LWP)根本不是一回事

别被“线程”这个词带偏。linux 内核里压根没有“线程”这个独立调度对象的抽象,只有 task_struct。用户态的 pthread 线程本质是多个共享 mm_structtask_struct(即 LWP),而内核线程的 task_struct->mm == NULL,且它的 active_mm 是借用前一个用户进程的——这是 lazy TLB 切换的关键优化。

这意味着:

  • 内核线程不能访问用户地址空间,copy_from_user() 在它上下文中会直接 panic;
  • 它无法使用 get_user_pages() 或 mmap 相关接口,除非先显式切换 mm(极少见且危险);
  • 调试时用 /proc/PID/stack 看到的调用全是内核函数,没有 libc 或用户符号。

怎么创建一个内核线程?别手写 kernel_thread()

老教程常提 kernel_thread() 这个底层接口,但它已标记为 __deprecated,现代驱动和子系统都应改用 kthread_run()kthread_create() + wake_up_process()

例如:

struct task_struct *tsk = kthread_run(my_worker_fn, data, "mykthread");

如果返回值是 IS_ERR(tsk),说明创建失败(比如内存不足或内核线程数超限),必须检查;成功后线程自动运行,函数退出即线程终止——不需要手动调用 do_exit()

容易踩的坑:

  • 传给线程函数的 data 必须是全局或 kmalloc 分配的,不能是变量(线程可能在原函数返回后才开始执行);
  • 线程函数内部若需长时间等待,务必用 wait_event_interruptible() 而非 while(1) + schedule(),否则可能被 sysrq+T 杀死时无法响应;
  • 不要在内核线程里调用可能 sleep 的用户态路径(如 filp_open()),除非你确认当前上下文允许阻塞(比如已用 allow_signal() 配置过)。

最常被忽略的一点:内核线程没有文件描述符表、不继承信号掩码、也不参与 cgroup 的 cpuacct 统计——它只属于 init_css_set,这点在容器环境排查资源归属时特别容易误判。

text=ZqhQzanResources