内核眼里的进程是一个 task_struct,几千行的大结构,身份,亲属,内存,调度,文件,信号全在里面.这篇不逐字段背书,挑最常用的几组 — 状态,PID 体系,亲缘关系 — 全部在板上现场制造现场观察:一个测试程序同时造出睡眠,僵尸,停止三种状态,再加一组多线程,拿 /proc 逐个解剖.
这篇回答几件事:
- task_struct 的状态有哪几种,怎么现场制造和观察
- PID,TGID,TID 三者什么关系,/proc 里怎么对应
- 进程树怎么组织,僵尸挂在哪,内核线程算什么
- comm 和 cmdline 的区别
先造现场.一个小程序,fork 一个子进程立刻退出(父进程故意不 wait,制造僵尸),再 fork 一个子进程给它发 SIGSTOP(制造停止态),自己起两个 pthread 后 pause 睡大觉.两秒后解剖,三种状态同屏:
pid=1778 | State: S (sleeping) | comm=states
pid=1780 | State: Z (zombie) | comm=states
pid=1781 | State: T (stopped) | comm=states
S 睡眠,等事件(这里是 pause 等信号);Z 僵尸,进程已死但父进程没收尸,task_struct 还占着坑 — 这是给父进程留的"死亡证明",退出码和资源统计要等人来读;T 停止,SIGSTOP 送达后的冻结态,SIGCONT 解封.加上 R 运行和 D 不可中断睡眠,五个字母就是 ps 里那列 stat 的全部主角.R 和 S 大家熟,D 是在做不可中断 IO(读 NFS 断网最经典)的态,信号都叫不醒,kill -9 也没用,只能等 IO 超时或硬件恢复.实验里还顺带抓到停止态进程的上下文切换账本:/proc/PID/status 里 voluntary_ctxt_switches: 2,nonvoluntary: 0 — 自愿切换(主动睡眠)和被抢占各记各的,后面讲调度时这对数字还能用.
PID 体系是第二个重点,概念坑密度最高.先把实验数据摆出来,主进程 1874 带两个线程:
Threads: 3
Tgid: 1874
Pid: 1874
/proc/1874/task/ -> 1874 1878 1879
内核里每个线程一个 task_struct,task 的 pid 字段其实是"线程号"(TID);线程组组长的 TID 兼任组长 PID,同一组所有成员的 tgid 字段都指向它./proc 顶层目录按 TGID 列(所以 ps 看到的是"进程"),组内每个线程的详情在 /proc/TGID/task/TID 下 — 1878 和 1879 这两个线程没有顶层目录,但它们的 sched,status 一应俱全.用户态 API 对应关系:getpid() 拿 TGID,全组一致;gettid() 拿自己的 TID,每线程不同;kill(pid) 默认发给整个组,tkill/tgkill 才能精确打线程.这条对应链弄通,"线程是轻量进程"就不只是口号:在内核里它们就是同一种对象,差在组指针和资源共享量.
亲缘关系,task_struct 里三把链表:real_parent(生我的),parent(平时同 real,被 ptrace 时换人),children(双向),再加 sibling 串起同父的兄弟.实测僵尸进程 1876 的 PPid: 1874 — 父进程活着但从不 wait,僵尸就永远挂在父亲的 children 链上.父母先死孩子未亡的,孤儿被过继给最近的 subreaper 或 PID 1(板上 Debian 的 init 是 systemd),这是容器里"僵尸泄漏"问题的根源:容器里 PID 1 如果不 wait,僵尸在容器内永远清不掉,而这些僵尸在宿主机看来又在 systemd 名下 — 子收mitter 的责任链要一路通到会收尸的祖先.内核线程是另一族:全员的祖先是 PID 2 的 kthreadd(2.2 篇看过它的诞生),它们没有用户地址空间,/proc/2/task 下只有一个 tid,和用户线程不混编.
名字的两组字段别混.comm 是内核视角的名字,16 字节截断,/proc/PID/status 的 Name 和 ps 默认显示的都是它;cmdline 是 argv 原文,NUL 分隔,一个进程可以带着一长串参数跑,comm 却只有程序名.实验里 comm=states 而 cmdline=./states.改 comm 用 prctl(PR_SET_NAME) — 有些多线程服务给每个线程改 comm 标角色,top 里一眼分清谁是谁,调试期特别好使.
最后留几个容易忽略的细节:
- R 状态包含"就绪但没跑上 CPU",单核跑两个死循环,ps 看到两个 R;Linux 没有"就绪队列里排队"这个独立状态,进队列就是 R.
- 僵尸不占内存只占 PID 和 task_struct,但 PID 是有限资源(约 400 万,内核参数可调),fork 炸弹式的僵尸泄漏能把号吃光,之后全系统无法建新进程.
- /proc/PID/stat 的字段 3 是父 PID,但被自动收养后看到的是收养者,"亲生父母"查不回来,要追 fork 链得上 eBPF 或 audit.
- Threads 字段报的是整个线程组的线程数,组长和成员的 status 里读出来一样;想看单个线程自己的调度细节,进 /proc/TGID/task/TID/ 下的同名文件,视角完全不同.
- D 状态风暴是存储问题的经典信号:ps 里一片 D,先查 IO 子系统,别杀进程,杀了也没反应.
- task_struct 太大,内核不栈上分配它,专门的 slab 缓存 task_struct_cachep 伺候,fork 快慢和这个缓存命中率有关.
- PID namespace 会改变 /proc 的编号视角,容器里 PID 1 在宿主机可能是 3628,同一对象两套号,/proc 读数要带着 namespace 意识.