Skip to content

第三十章:kthreadd 婶婶

筑基中期

涉及内核源码:

林小源终于见到了 kthreadd 婶婶。

她是 PID 2——仅次于 init 童子的存在。但她不像 init 童子那样引人注目。她不管理用户态进程,不回收僵尸,不处理信号。她的工作更安静:创建和管理所有的内核线程。

内核线程和用户态进程不同。它们没有用户态地址空间(NULL),只在内核态运行。它们不执行用户态程序,不处理系统调用。它们是内核的"内部工作者"——处理磁盘 I/O、内存回收、网络包处理等底层任务。

c
/*
 * 内核线程的特点:
 * 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");

内核自己也需要一群"打工人"。

林小源第一次和 kthreadd 婶婶说话时,她正在创建一个新的工作队列线程。

kthreadd 婶婶不像 init 童子那样傲慢,也不像 shell 小妹那样活泼。她安静、专注,几乎不说话。她的世界很简单:收到请求,创建线程,继续等待。

"你好。"林小源说。

kthreadd 婶婶看了他一眼,没有说话。

"我是林小源,PID 0。"

"我知道。"kthreadd 婶婶说。她的声音很轻,像内核日志中最低级别的 。"你就是那个被 #ifdef 0 封印的代码。"

林小源愣住了。"你知道我的来历?"

kthreadd 婶婶没有回答。她继续创建线程—— 一个新的 ,设置内核栈,设置 mm = NULL,设置 [kworker/0:1]。整个过程不到一毫秒。

"有些事情,"kthreadd 婶婶终于开口,"不是不想告诉你,是不能告诉你。"

她知道什么?

c
/*
 * 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");

林小源看着 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 创建的内核线程没有用户态地址空间,正文用哪个字段状态表示这一点?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道