Skip to content

第十七章:task_struct 之躯

筑基初期

涉及内核源码:

林小源决定先理解自己的"肉身"。

是进程的身份证、户口本、病历、档案——所有关于一个进程的信息都记录在这里。他在前传中就看过 的简化版本,但真正的 有数百个字段,涵盖了一个进程从诞生到死亡的方方面面。

他开始逐字段地阅读。

调度相关的字段最先吸引了他的注意。 是进程当前的状态——RUNNING、INTERRUPTIBLE、UNINTERRUPTIBLE、STOPPED、ZOMBIE。 是动态优先级, 是静态优先级。 是调度策略——SCHED_NORMAL、SCHED_FIFO、SCHED_RR。 是调度实体,包含了 CFS 需要的 和权重。

这些字段决定了我什么时候能运行。

身份信息也很重要。 是进程 ID, 是线程组 ID(对于非线程进程,tgid == pid)。 是进程名,最多 15 个字符加上一个 \0 指向创建这个进程的父进程, 指向当前的父进程(可能被 修改)。 是子进程链表, 是兄弟进程链表。

"进程居然也有'家族关系'。"林小源低声自语。

"你以为进程是孤零零地活着的?"一个沙哑的声音从旁边传来。

林小源转头一看,一个身披灰色长袍的老者正坐在一块巨大的结构体上——那结构体足有一间屋子那么大,表面密密麻麻地刻满了字段名。老者自称"户籍仙翁",是 的守护者。

"你看这里,"户籍仙翁拍了拍身下的结构体,指着 字段,"每个进程都有父亲。 是生父——谁调用 创建了你,谁就是你的 。但 可以被改——如果有人用 调试你, 就指向调试者。"

"那 呢?"

" 链表把你所有的子进程串起来。 链表把你和你的兄弟姐妹串起来。整个进程树就是这样构建的——从 init 开始,一层一层往下。"户籍仙翁顿了顿,"你虽然是 idle,但你也有父亲——内核的 函数创建了你。"

c
/* 简化的 task_struct 身份字段 */
struct task_identity {
    int pid;              /* 进程 ID */
    int tgid;             /* 线程组 ID */
    char comm[16];        /* 进程名 */
    int uid;              /* 用户 ID */
    int gid;              /* 组 ID */
    int euid;             /* 有效用户 ID */
    int egid;             /* 有效组 ID */
    int exit_state;       /* 退出状态 */
    int exit_code;        /* 退出码 */
};

struct task_identity bash = {
    .pid = 1234,
    .tgid = 1234,
    .uid = 1000,
    .gid = 1000,
    .euid = 1000,
    .egid = 1000,
    .exit_state = 0,
    .exit_code = 0,
};
__builtin_memcpy(bash.comm, "bash", 5);

printf("=== task_struct 身份字段 ===\n\n");
printf("进程名:    %s\n", bash.comm);
printf("PID:       %d\n", bash.pid);
printf("TGID:      %d\n", bash.tgid);
printf("UID:       %d\n", bash.uid);
printf("EUID:      %d\n", bash.euid);
printf("GID:       %d\n", bash.gid);

printf("\n--- uid vs euid ---\n");
printf("uid  = 实际用户 ID(谁启动的进程)\n");
printf("euid = 有效用户 ID(决定权限检查)\n");
printf("当 setuid 程序运行时,euid != uid\n");
printf("例如: passwd 命令 euid=0(root),uid=1000(普通用户)\n");

户籍仙翁指着 两个字段,语气变得严肃起来:"这两个字段的区别,你必须搞清楚。 是你的出身——谁启动了你,就是谁的 。但 是你当下的权力——权限检查看的是它,不是 。"

"那 程序呢?"

"聪明,"户籍仙翁赞许地点头," 程序运行时, 被设为文件所有者的 ID——通常是 root。所以一个普通用户的 是 1000,但运行 变成 0。程序以 root 权限做事,但做完了权限就收回去。这就是最小权限原则——只在需要的时候给需要的权限,多一分都不给。"

林小源在 的区别中看到了一种"权限委托"的机制。精妙,但不复杂。

内存管理相关的字段让林小源花了更多时间。

指向 ——进程的地址空间描述符。 包含了虚拟地址空间的布局:代码段的起始和结束地址、数据段的起始和结束地址、堆的起始地址、栈的起始地址。它还包含了页表的根指针(在 RISC-V 上是 寄存器的值)和一个管理所有虚拟内存区域(VMA)的红黑树。

"这下他懂了——地址空间就是靠这套结构管起来的。"林小源感慨。

户籍仙翁在一旁补充道:"你看到的 只是一个壳子。真正的地址空间在那棵红黑树里——每个 VMA 就是一块连续的虚拟地址区域,有自己的权限和属性。代码段是 r-x,数据段是 rw-,栈是 rw-。内核在分配内存时,就是在这棵树上查找和插入节点。"

文件相关的字段也很有趣。 指向 ,它包含了进程打开的所有文件描述符。每个文件描述符是一个整数索引,指向 struct file 的指针数组。struct file 包含了文件的当前位置()、打开模式()、以及指向 的指针。

"文件描述符就是进程与外界沟通的桥梁。"林小源说。

"没错,"户籍仙翁应道,"0 是标准输入,1 是标准输出,2 是标准错误。shell 的管道重定向,本质上就是修改这些文件描述符的指向。"

信号相关的字段是最后一批。 指向 ,它包含了信号队列、信号掩码、以及定时器。 指向 ,它包含了每个信号的处理函数。

林小源在 中看到了一个进程的"全息图"——从身份到权限,从内存到文件,从信号到调度,所有信息都在这一个结构体中。

就在林小源研究 的过程中,他注意到了一个有趣的现象。

kthreadd 婶婶——PID 2——在默默地创建内核线程。每隔一段时间,就会有一个新的内核线程被创建出来,执行某个特定的任务,然后在任务完成后睡眠。

林小源通过 字段看到了这些内核线程的名字:kworker/0:0ksoftirqd/0。每一个都有自己的职责,每一个都是 kthreadd 婶婶创建的。

"她就像一个沉默的工匠。"林小源对户籍仙翁说。

户籍仙翁沉默了一会儿,才缓缓开口:"kthreadd……她从不说多余的话。你见过她的 字段吗?永远是 'kthreadd',从不改变。不像有些进程,名字换来换去的。"

"她不累吗?"

"累?"户籍仙翁苦笑了一下,"她连'累'这个概念都没有。她就是一个函数—— 函数,永远在循环里等待,收到请求就创建内核线程,创建完了继续等。没有休息,没有结束。"

kthreadd 婶婶不说话,不炫耀,不争抢 CPU 时间。她只是在需要的时候创建内核线程,然后继续她的等待。林小源从未听她说过一句话——她连 字段都是固定的 "kthreadd",从不改变。

但她的工作是不可或缺的。没有 kthreadd 婶婶,内核就无法创建内核线程。没有内核线程,软中断、RCU、工作队列等机制就无法运转。她是内核世界的"后勤部长"——不显眼,但不可或缺。

林小源望着她忙碌的背影,忽然觉得有些惭愧。他还在为自己能不能理解 而焦虑,而 kthreadd 婶婶已经在默默地支撑着整个内核的运转了。


道藏笔记

内核启示

是进程的"全息图"。

你打开一个 ,就像翻开一个人的档案:调度信息告诉你它什么时候能跑(、调度实体),身份信息告诉你它是谁(///),家族关系告诉你它的来龙去脉()。再往里翻,内存管理有 管着地址空间,文件系统有 管着打开的文件、 管着当前目录,信号有 处理,安全方面还有 管凭证、 挂着 LSM 安全模块的数据。

这里面最容易搞混的是 是你的出身——谁启动的你,就是谁的 。但 才是你当下的权力——权限检查看的是它。 程序就是利用这个区别,让 临时变成 root,做完事再收回来。

理解 task_struct,就是理解一个进程的全部。


破关试炼

肉身之试

本章把进程的肉身拆开讲解,内核用哪一种结构体保存进程的身份、状态和调度信息?

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

以修仙之名,悟内核之道