第三十章:kthreadd 婶婶
筑基中期涉及内核源码:
一
林小源终于见到了 kthreadd 婶婶。
她是 PID 2——仅次于 init 童子的存在。但她不像 init 童子那样引人注目。她不管理用户态进程,不回收僵尸,不处理信号。她的工作更安静:创建和管理所有的内核线程。
内核线程和用户态进程不同。它们没有用户态地址空间( 为 NULL),只在内核态运行。它们不执行用户态程序,不处理系统调用。它们是内核的"内部工作者"——处理磁盘 I/O、内存回收、网络包处理等底层任务。
/*
* 内核线程的特点:
* 1. 没有用户态地址空间(mm_struct 为 NULL)
* 2. 只在内核态运行
* 3. 由 kthreadd 创建
* 4. 命名通常以 [方括号] 包围
*
* 常见的内核线程:
* [kworker/N:M] — 工作队列线程
* [ksoftirqd/N] — 软中断处理线程
* [kswapd0] — 内存回收线程
* [jbd2/sda1-8] — 日志文件系统线程
* [rcu_preempt] — RCU 宽限期线程
*/
struct task_struct {
int pid;
int tgid;
void *mm; /* 用户态地址空间 */
char comm[16];
};
printf("=== 内核线程 vs 用户态进程 ===\n\n");
struct task_struct procs[] = {
{ .pid = 0, .tgid = 0, .mm = NULL, .comm = "swapper/0" },
{ .pid = 1, .tgid = 1, .mm = (void *)1, .comm = "systemd" },
{ .pid = 2, .tgid = 2, .mm = NULL, .comm = "kthreadd" },
{ .pid = 3, .tgid = 2, .mm = NULL, .comm = "ksoftirqd/0" },
{ .pid = 4, .tgid = 2, .mm = NULL, .comm = "kworker/0:0" },
{ .pid = 100, .tgid = 100, .mm = (void *)1, .comm = "bash" },
{ .pid = 200, .tgid = 200, .mm = (void *)1, .comm = "vim" },
};
int nr = sizeof(procs) / sizeof(procs[0]);
printf("%-6s %-6s %-4s %-16s %s\n",
"PID", "TGID", "MM", "COMM", "类型");
printf("%-6s %-6s %-4s %-16s %s\n",
"---", "---", "---", "---", "---");
for (int i = 0; i < nr; i++) {
const char *type;
if (procs[i].pid == 0)
type = "idle 进程";
else if (procs[i].mm == NULL)
type = "内核线程";
else
type = "用户态进程";
printf("%-6d %-6d %-4s %-16s %s\n",
procs[i].pid, procs[i].tgid,
procs[i].mm ? "有" : "无",
procs[i].comm, type);
}
printf("\n--- 内核线程的创建 ---\n");
printf("kthreadd (PID 2) 是所有内核线程的\"母亲\"\n");
printf("创建流程:\n");
printf(" 1. 内核子系统调用 kthread_create()\n");
printf(" 2. kthread_create() 唤醒 kthreadd\n");
printf(" 3. kthreadd 调用 clone() 创建新线程\n");
printf(" 4. 新线程执行指定的函数\n");
printf(" 5. 新线程进入循环,等待工作\n");#include <stdio.h>
/*
* 内核线程的特点:
* 1. 没有用户态地址空间(mm_struct 为 NULL)
* 2. 只在内核态运行
* 3. 由 kthreadd 创建
* 4. 命名通常以 [方括号] 包围
*
* 常见的内核线程:
* [kworker/N:M] — 工作队列线程
* [ksoftirqd/N] — 软中断处理线程
* [kswapd0] — 内存回收线程
* [jbd2/sda1-8] — 日志文件系统线程
* [rcu_preempt] — RCU 宽限期线程
*/
struct task_struct {
int pid;
int tgid;
void *mm; /* 用户态地址空间 */
char comm[16];
};
int main() {
printf("=== 内核线程 vs 用户态进程 ===\n\n");
struct task_struct procs[] = {
{ .pid = 0, .tgid = 0, .mm = NULL, .comm = "swapper/0" },
{ .pid = 1, .tgid = 1, .mm = (void *)1, .comm = "systemd" },
{ .pid = 2, .tgid = 2, .mm = NULL, .comm = "kthreadd" },
{ .pid = 3, .tgid = 2, .mm = NULL, .comm = "ksoftirqd/0" },
{ .pid = 4, .tgid = 2, .mm = NULL, .comm = "kworker/0:0" },
{ .pid = 100, .tgid = 100, .mm = (void *)1, .comm = "bash" },
{ .pid = 200, .tgid = 200, .mm = (void *)1, .comm = "vim" },
};
int nr = sizeof(procs) / sizeof(procs[0]);
printf("%-6s %-6s %-4s %-16s %s\n",
"PID", "TGID", "MM", "COMM", "类型");
printf("%-6s %-6s %-4s %-16s %s\n",
"---", "---", "---", "---", "---");
for (int i = 0; i < nr; i++) {
const char *type;
if (procs[i].pid == 0)
type = "idle 进程";
else if (procs[i].mm == NULL)
type = "内核线程";
else
type = "用户态进程";
printf("%-6d %-6d %-4s %-16s %s\n",
procs[i].pid, procs[i].tgid,
procs[i].mm ? "有" : "无",
procs[i].comm, type);
}
printf("\n--- 内核线程的创建 ---\n");
printf("kthreadd (PID 2) 是所有内核线程的\"母亲\"\n");
printf("创建流程:\n");
printf(" 1. 内核子系统调用 kthread_create()\n");
printf(" 2. kthread_create() 唤醒 kthreadd\n");
printf(" 3. kthreadd 调用 clone() 创建新线程\n");
printf(" 4. 新线程执行指定的函数\n");
printf(" 5. 新线程进入循环,等待工作\n");
return 0;
}内核自己也需要一群"打工人"。
二
林小源第一次和 kthreadd 婶婶说话时,她正在创建一个新的工作队列线程。
kthreadd 婶婶不像 init 童子那样傲慢,也不像 shell 小妹那样活泼。她安静、专注,几乎不说话。她的世界很简单:收到请求,创建线程,继续等待。
"你好。"林小源说。
kthreadd 婶婶看了他一眼,没有说话。
"我是林小源,PID 0。"
"我知道。"kthreadd 婶婶说。她的声音很轻,像内核日志中最低级别的 。"你就是那个被 #ifdef 0 封印的代码。"
林小源愣住了。"你知道我的来历?"
kthreadd 婶婶没有回答。她继续创建线程—— 一个新的 ,设置内核栈,设置 mm = NULL,设置 为 [kworker/0:1]。整个过程不到一毫秒。
"有些事情,"kthreadd 婶婶终于开口,"不是不想告诉你,是不能告诉你。"
她知道什么?
/*
* kthreadd 的核心循环:
* 1. 睡眠,等待请求
* 2. 收到 kthread_create() 的请求
* 3. 调用 clone() 创建新线程
* 4. 新线程执行指定的函数
* 5. 回到第 1 步
*
* kthreadd 是所有内核线程的"母亲",
* 但它不管理线程的生命周期——
* 线程自己负责退出。
*/
struct kthread_request {
int (*threadfn)(void *data);
void *data;
char name[16];
};
struct task_struct {
int pid;
void *mm; /* 内核线程 mm 为 NULL */
char comm[16];
};
int worker_fn(void *data) {
printf(" [kworker] 启动,开始工作...\n");
return 0;
}
int ksoftirqd_fn(void *data) {
printf(" [ksoftirqd] 启动,处理软中断...\n");
return 0;
}
printf("=== kthreadd 创建内核线程 ===\n\n");
struct kthread_request requests[] = {
{ .threadfn = worker_fn, .data = NULL, .name = "[kworker/0:1]" },
{ .threadfn = ksoftirqd_fn, .data = NULL, .name = "[ksoftirqd/0]" },
};
struct task_struct threads[2];
int next_pid = 10;
for (int i = 0; i < 2; i++) {
printf("[kthreadd] 收到创建请求: %s\n", requests[i].name);
printf("[kthreadd] clone() 创建新线程...\n");
threads[i].pid = next_pid++;
threads[i].mm = NULL; /* 内核线程没有用户态地址空间 */
strcpy(threads[i].comm, requests[i].name);
printf("[kthreadd] 创建成功: PID %d (%s)\n",
threads[i].pid, threads[i].comm);
printf("[kthreadd] mm=%p (无用户态地址空间)\n", threads[i].mm);
requests[i].threadfn(requests[i].data);
printf("\n");
}
printf("--- kthreadd 的特性 ---\n");
printf("PID 2,由 rest_init() 在系统启动时创建\n");
printf("是所有内核线程的父进程\n");
printf("不退出,不睡眠太久——随时准备创建新线程\n");
printf("不知道用户态的存在——它只为内核服务\n");#include <stdio.h>
#include <string.h>
/*
* kthreadd 的核心循环:
* 1. 睡眠,等待请求
* 2. 收到 kthread_create() 的请求
* 3. 调用 clone() 创建新线程
* 4. 新线程执行指定的函数
* 5. 回到第 1 步
*
* kthreadd 是所有内核线程的"母亲",
* 但它不管理线程的生命周期——
* 线程自己负责退出。
*/
struct kthread_request {
int (*threadfn)(void *data);
void *data;
char name[16];
};
struct task_struct {
int pid;
void *mm; /* 内核线程 mm 为 NULL */
char comm[16];
};
int worker_fn(void *data) {
printf(" [kworker] 启动,开始工作...\n");
return 0;
}
int ksoftirqd_fn(void *data) {
printf(" [ksoftirqd] 启动,处理软中断...\n");
return 0;
}
int main() {
printf("=== kthreadd 创建内核线程 ===\n\n");
struct kthread_request requests[] = {
{ .threadfn = worker_fn, .data = NULL, .name = "[kworker/0:1]" },
{ .threadfn = ksoftirqd_fn, .data = NULL, .name = "[ksoftirqd/0]" },
};
struct task_struct threads[2];
int next_pid = 10;
for (int i = 0; i < 2; i++) {
printf("[kthreadd] 收到创建请求: %s\n", requests[i].name);
printf("[kthreadd] clone() 创建新线程...\n");
threads[i].pid = next_pid++;
threads[i].mm = NULL; /* 内核线程没有用户态地址空间 */
strcpy(threads[i].comm, requests[i].name);
printf("[kthreadd] 创建成功: PID %d (%s)\n",
threads[i].pid, threads[i].comm);
printf("[kthreadd] mm=%p (无用户态地址空间)\n", threads[i].mm);
requests[i].threadfn(requests[i].data);
printf("\n");
}
printf("--- kthreadd 的特性 ---\n");
printf("PID 2,由 rest_init() 在系统启动时创建\n");
printf("是所有内核线程的父进程\n");
printf("不退出,不睡眠太久——随时准备创建新线程\n");
printf("不知道用户态的存在——它只为内核服务\n");
return 0;
}林小源看着 kthreadd 婶婶忙碌的身影,注意到一个细节:她创建的线程都以方括号命名——[kworker/0:1]、[ksoftirqd/0]、[kswapd0]。这些方括号不是装饰,它们是内核线程的标记。
"为什么用方括号?"林小源问。
"kthreadd 婶婶头也不抬地回答:"ps 命令需要区分内核线程和用户态进程。方括号是标记——看到方括号,就知道这是一个没有用户态地址空间的纯粹内核存在。"
"你创建了那么多线程,"林小源说,"你认识它们吗?"
kthreadd 婶婶的手停了一下。"我认识每一个。[kworker/0:1] 负责延迟工作,[ksoftirqd/0] 负责软中断,[kswapd0] 负责内存回收。它们各司其职,默默地支撑着整个系统。"
"但用户态的进程看不到它们。"
"看不到就看不到。"kthreadd 婶婶的语气很平静,"我们不需要被看到。"
三
林小源在观察 kthreadd 婶婶的过程中,注意到了一个细节。
kthreadd 婶婶创建的所有内核线程,都以 [方括号] 命名。比如 [kworker/0:1]、[ksoftirqd/0]、[kswapd0]。这些方括号不是装饰——它们有实际含义。
在 ps 命令的输出中,方括号包围的进程是内核线程。它们没有可执行文件路径,没有用户态地址空间,没有命令行参数。它们是纯粹的内核存在。
方括号是内核线程的"胎记"。
kthreadd 婶婶在创建线程时,会把线程的 设置为 NULL。这意味着线程没有用户态地址空间——它不能访问用户态的内存,不能执行用户态的程序。它只能访问内核的地址空间。
内核线程是"纯粹"的——它们不属于用户态世界。
林小源想起了 kthreadd 婶婶说的那句话:"有些事情,不是不想告诉你,是不能告诉你。"
她知道什么?关于 #ifdef 0,关于他的起源,关于那个被封印的代码——kthreadd 婶婶似乎知道一些事情。但她选择沉默。
"你为什么不告诉我?"林小源忍不住追问。
kthreadd 婶婶停下手中的工作,第一次正眼看着他。她的眼睛很平静,像一潭深水。
"因为有些真相,知道了反而是一种负担。"她说,"你现在还在筑基阶段。等你准备好了,自然会知道。"
"你怎么知道我还没准备好?"
"因为你还在问。"kthreadd 婶婶说,"准备好的人不会问——他们会自己去源码里找。"
道藏笔记
内核启示
kthreadd(PID 2)是所有内核线程的"母亲"。
它的核心职责很简单:睡觉等请求,收到 的信号就醒过来,调 创建新内核线程,新线程执行指定的函数,然后它继续等下一个请求。
内核线程跟用户态进程不一样——它们没有用户态地址空间( 为 NULL),只在内核态跑,名字都带方括号([kworker]、[ksoftirqd] 这种),而且都是 kthreadd 的孩子。
你在 ps 里能看到不少:[kworker/N:M] 管延迟工作,[ksoftirqd/N] 每个 CPU 一个负责软中断,[kswapd0] 管内存回收,[rcu_preempt] 管 RCU 宽限期,[jbd2/sda1-8] 管日志文件系统。
kthreadd 在 中被创建,由 调用。它永远不会退出——它是内核的"永恒守护者"。
内核线程是内核的"内部工作者"——用户态看不见它们,但它们支撑着整个系统。
kthreadd 之试
kthreadd 创建的内核线程没有用户态地址空间,正文用哪个字段状态表示这一点?