进程基础 — 进程树、ELF 与内存映像、/proc¶
这一章讲什么: 进程是「跑起来的程序」,但这句话里藏着四件 可以亲手拆开的东西:组织(系统里几百个进程的父子/分组关系)、 程序文件(可执行文件里装着重建一个进程所需的全部图纸——ELF 格式)、内存映像(进程的虚拟地址空间里哪段放代码、哪段放数据、 堆栈往哪儿长)、以及内核的账本(task_struct 和它的 /proc 窗口)。 项目:打印 ELF 程序头表、ancestors(祖先链)、spl_ps。 读完你会真正看懂
ps、top、/proc和段错误。
1. 顶层全景:一个进程的四面¶
组织面:进程树(init=PID 1 是根)→ 进程组(管道里的进程同组,
CTRL-C 一组全灭)→ 会话(登录 shell 及其后代;前后台之分;
会话首领脱离终端 = daemon 的雏形)
│
文件面:可执行文件 = text + 入口点 + 重定位表 + 符号表 + 字符串表
+ 已初始化数据 + 动态链接信息,按 ELF 格式组织
(链接视图=节,执行视图=段;同一个文件两套目录)
│
内存面:虚拟地址空间五区——text(只读可共享)→ data(已初始化)
→ BSS(未初始化,不占文件空间)→ heap(malloc,往上长)
→ stack(往下长);argv/envp 在栈顶;内核映射在最顶上
│
内核面:task_struct(≈300 成员,指针连子结构)——进程 ID 家族、
文件表、信号队列、定时器、内存图;内核不直接暴露它,
只开一扇窗:/proc 伪文件系统
图说:前两面回答「进程从哪来」,后两面回答「内核怎么管、 我们怎么看」。 主线例子:displayvm 程序打印它自己体内 15 个 符号的虚拟地址,从高到低排——这张表就是内存布局的活 X 光片, 每个数都有出处。
2. 组织面:进程树、进程组与会话¶
系统的几百个进程,除了 init(PID 1,现在是 systemd)是内核
亲手造的,全是别的进程生的——父子关系连成进程树,
pstree 能画出来1。
进程组是「作业」的抽象:管道 cat | sort | uniq | wc 里四个
进程被 shell 放进同一个新组,组长的 PID 就是组 ID(PGID)——
ps -opid,ppid,pgrp,cmd 能看到 cat 的 PID==PGRP,其余三人的
PGRP 都等于它。CTRL-C 一发 SIGINT,整组齐收;用命令行打就是
kill -SIGINT -16779(负号 = 给全组)2。改组的系统调用:
setpgid(pid, pgid)(变体四种:0 表示自己/pgid 表示目标组)、
getpgrp()/getpgid(p) 查3。
会话是「登录」的抽象:一次登录创建的所有进程组同属一个会话,
首领的 SID==PID。前台组最多一个,能读控制终端;后台组读终端会
收 SIGTTIN、写收 SIGTTOU(被停,不是被杀);键盘信号只发
前台组。SIGHUP 发给全会话——断线、登出时后台进程默认全灭,
nohup 就是让程序忽略 SIGHUP 的4。
setsid():非组长进程调它 → 新建会话(SID=自己 PID)+ 新进程组 (组长是自己)+ 没有控制终端(从此免疫键盘信号)。这就是 daemon(守护进程)的出生证明——第 14 章的 daemon 化流程第一步 就是它5。
3. 文件面:ELF——一个文件,两张视图¶
可执行文件最少要装:机器指令(text)、入口点、重定位表、符号表、 字符串表、已初始化数据、动态库清单和动态链接器路径6。现代 Unix 用 ELF(1993 年 UNIX System Laboratories 发布,取代 a.out;"a.out" 本是 "assembler output")。ELF 有四类文件:可重定位 目标文件、可执行文件、共享对象、core 文件7。
ELF 的双视图是全章的枢纽8:
| 链接视图(给链接器) | 执行视图(给加载器) | |
|---|---|---|
| 单位 | 节(section) | 段(segment) |
| 目录 | 节头表(在文件尾) | 程序头表(紧跟 ELF 头) |
| 内容 | .text/.rodata/.data/.bss + 符号表 + 重定位 | 装入内存的 LOAD 段 |
| 谁必须有 | 链接期文件 | 可执行文件 |
图说:段由一个或多个相邻的节组成——.text .init .plt .fini
同进一个 LOAD 段(R E 权限),.rodata 进另一个(R),.data
.bss 进第三个(RW);有的节还不属于任何段(如 GNU_RELRO 是第 5
段的子集,动态链接器靠它标记「重定位后改只读」)9。文件头
readelf -h 开头 4 字节恒为 7f 45 4c 46(\x7fELF),第 5 字节
区分 32/64 位;头里 e_phoff/e_phentsize/e_phnum 三个数定位整张
程序头表10。
项目:print_elfphdr——自己当 readelf¶
思路就是把「路标」走一遍:读 16 字节 e_ident → 确认 ELFCLASS64 →
按 Elf64_Ehdr 结构(elf.h 定义)读整个头 → calloc
e_phnum × sizeof(Elf64_Phdr) → lseek 到 e_phoff 读入整张程序头
表 → 逐条打印;遇到 INTERP 段特殊处理——段的内容就是动态
链接器路径字符串,按 p_offset/p_filesz 读出来打印11。
一个 hello_world:13 个程序头、31 个节头,LOAD 段按 0x1000
对齐12。
4. 内存面:五段布局与实测¶
x86-64 Linux 的虚拟地址空间,从低到高13:
高地址 ─┬─ 内核映射(用户态不可访问;系统调用时切特权级后可用;
│ 含每进程内核栈——内核栈大小可预测、有固定边界)
├─ 环境字符串 / 命令行参数 / argc,argv,envp
├─ 用户栈(往下长;每次函数调用压一帧:局部变量+返回地址)
├─ 空闲区(访问即异常)
├─ 动态链接库(mmap 进来;strcpy/write 住这里)
├─ 堆(malloc;program break 为顶,sbrk 移动它;往上长)
├─ BSS(未初始化全局/静态变量;loader 只预留+填 0;
│ 不占 ELF 文件空间)
├─ data(已初始化全局/静态变量)
低地址 ─┴─ text(代码;只读、可共享——全系统一份物理拷贝,
换出不写盘)
之下是动态链接器映射区(不可访问)
ASLR(地址空间布局随机化):各区的相对位置不变,起点加
随机偏移——所以 text 不再固定在 0x08048000;编译器配合生成
位置无关代码(PIE)14。C 库提供三个路标符号:
extern char etext, edata, end(text 尾、data 尾、BSS 尾)。
主线例子走查:displayvm 的 15 个地址¶
displayvm 打印自己体内符号的地址,从高到低(真实运行值, 每次跑因 ASLR 不同):
envp 7FFE101EA268 ← 最高:栈顶,环境指针数组
argv 7FFE101EA258
i 7FFE101E9F88 ← 栈:局部变量
argc 7FFE101E9F7C
strcpy 7FD86DD36CB0 ← 动态库区(比栈低、比堆高)
write 7FD86DCAC870
progbreak 561D306D8000 ← 堆顶(program break)
*inheap 561D306B72A0 ← malloc 来的首地址
end 561D2EB40148 ← BSS 尾
diff 561D2EB40140 ← BSS(static 局部变量)
string 561D2EB40040 ← BSS(未初始化全局;256 字节数组,
起点正好比 diff 低 0x100)
edata 561D2EB40018 ← data 尾
title 561D2EB40010 ← data(已初始化全局)
etext 561D2EB3D7D9 ← text 尾
main/sort 561D2EB3D39B/… ← text
图说:每一行都能指认自己住在哪一段——栈变量挨着 argv、
malloc 的地址在 end 之上、库的函数地址在堆和栈之间。
注意最低地址也有 0x561D2EB3D1F9 这么高:虚拟地址空间
(128TB 级)被 ASLR 撒得很开,虚拟大 ≠ 物理大,物理内存
按实际用的段分配15。sbrk(0) 只用来读 program break 的位置;
移动它请用 malloc/free,别直接调 brk/sbrk16。
5. 内核面:task_struct 与「换程序时什么保留」¶
Linux 把进程元数据聚合在 task_struct(进程描述符)里,6.13 版约 300 成员,本体加一堆子结构指针:thread_info、内存图、打开 文件表(files_struct)、信号结构(signal_struct:挂起队列、动作表)、 定时器、记账、tty 等。内核栈(系统调用时用的特权栈)也记在 里面——内核函数调用深度可预测,所以内核栈有固定边界17。
一个设计精妙处:换程序(exec)时元数据分两类——跟着「程序」
走的清空(内存图、栈、定时器、locale 等),跟着「进程」本身的
保留(PID/PPID/PGID/SID、真实 UID/GID、当前目录、根目录、
umask、信号掩码、挂起信号、alarm 剩余、忽略信号表、控制终端、
资源上限……)18。第 11 章会看到:exec 不换「身份」,fork 才
复制「身份」——这个分工正是 task_struct 分类设计的直接后果。
内核用 struct list_head { next, prev; } 小技巧让一个 task_struct
同时挂在进程总表、子进程表、兄弟表、线程组表多个双向链表上19。
6. /proc:内核的 X 光窗¶
/proc 是伪文件系统(1984 年第八版 UNIX 引入):没有一块
磁盘,全部内容是内核在 read 的瞬间从内部数据结构现场生成的——
读 /proc/uptime 就是调一个长得像 read 的内核函数
(proc_read)去读内存里的数20。所有进程的数据都在编号目录
/proc/PID/ 下;最有用的:cmdline(完整命令行,NULL 分隔)、
comm(可执行文件名)、cwd/exe(符号链接)、environ、
fd/(每个 fd 一个链接)、maps(内存映射)、status(人类
可读状态)、stat(一行全字段,ps 的数据源)21。
/proc/self 是个动态符号链接——谁读它,它就指向谁的目录
(cat /proc/self/comm 输出 cat)22。
项目:ancestors——顺着 PPid 爬¶
getparentid(p):拼出 /proc/p/status → getline 逐行读 →
sscanf(buf, "PPid: %d", ...) 抓到父 PID → 返回;main 里
while 循环直到 PPID 为 0(PID 1 的父是 0,到顶)23。
项目:spl_ps——读懂 /proc/[pid]/stat¶
/proc 下每个数字目录的 stat 是一行 52 个字段的序列。
proc(5) 给了每个字段的 scanf 格式。技巧:parse_buf 用一次
sscanf 读完,不想要的字段用 %*d 读取并丢弃(赋值抑制)24。
选出的字段要换算:
| 字段 | 换算 |
|---|---|
| utime+stime | 单位是 clock tick,先除 sysconf(_SC_CLK_TCK) 得秒,格式化成 HH:MM:SS |
| tty_nr | 一个数编码 (major,minor),major()/minor() 拆开,再按 /proc/tty/drivers 的号段表换成 "pts/2"、"tty11" 这种名 |
| start_time | tick 数 → 秒 → 加 /proc/stat 里的 btime(开机时刻) → 得到启动的日历时间;今天起的显 HH:MM、今年显 MonDD、更老显年份(ps 的三段式) |
| comm | 剥掉括号 |
| 用户名 | stat 文件里没有——对文件 fstat 拿 UID 再 getpwuid |
图说:ps 没有特权、不碰内核内部——它只是 /proc 的忠实读者25。
7. 作者的判断与证据¶
有明证的:
- 进程组/会话的语义(PGID=组长 PID、CTRL-C 发全组、SIGHUP 发 全会话、SIGTTIN/SIGTTOU)是 POSIX 定义,可对照 ps 输出验证24。
- ELF 的双视图、节段映射、INTERP 段内容,有 readelf 输出和 print_elfphdr 程序双重印证811。
- displayvm 的 15 个地址是真实运行值,且明确指出每次运行因 ASLR 不同15。
- task_struct「换程序时元数据分保留/清除两类」是内核设计的真实 描述,且对第 11 章的 fork/exec 语义有直接预言18。
作者的判断:
- 「spl_ps 只挑需要转换的字段来演示」——教学目标优先于命令 完备性,明说25。
- tty_name 用一大张 switch(major) 映射表——承认是笨办法, 但「够用了」;更通用的做法(读 /proc/tty/drivers 动态生成) 留作改进空间24。
- print_elfphdr 只处理 64 位——「扩展 32 位是小工程」,诚实 的教学裁剪11。
8. 边界与局限¶
- 内存布局是 x86-64 的典型布局,别的架构(栈的生长方向、 段的顺序)可能不同13。
- ELF 只讲了可执行文件;共享库的延迟绑定(PLT/GOT 的运转 细节)、core 文件格式没展开7。
- ASLR 只讲效果不讲实现(页表级随机化),PIE 只是点名14。
- /proc 的内容随内核版本演进,具体文件名和字段以你机器上的 proc(5) 为准21。
- spl_ps 不支持选字段、选进程、排序——真 ps 的几十项输出全是 同一套 /proc 读取加转换的组合25。
9. 可带走的¶
主线例子,一行复述: displayvm 从高到低打印 15 个地址—— 栈(argv/envp/局部变量)→ 动态库(strcpy/write)→ 堆(program break/malloc)→ BSS(end/diff/string)→ data(edata/title)→ text(etext/main/sort),一次运行看清五段布局。
- 进程树以 init(PID 1)为根;管道进程同组,组长 PID=PGID, CTRL-C 全组收;kill -PGID 按组发。
- 会话 = 登录创建的全部进程组;前台组才能读终端;后台读收 SIGTTIN;SIGHUP 杀全会话——nohup 是后台长寿的门票。
- setsid = 新会话 + 新组 + 无控制终端 = daemon 的第一步。
- 可执行文件 = text + 入口点 + 重定位/符号/字符串表 + 数据 + 动态链接信息;ELF 链接视图按节、执行视图按段,段=相邻节的组。
- readelf -h/-l/-S/-s 看 ELF;程序头表位置=e_phoff, 大小=e_phentsize×e_phnum。
- 虚拟内存五区:text(只读共享)/data/BSS(不占文件)/heap (往上)/stack(往下,装 argv/envp);ASLR 打乱起点、保留相对 位置。
- etext/edata/end 是三段尾的路标;sbrk(0) 看 program break, 但分配内存用 malloc。
- exec 换程序不换身份:PID/目录/umask/信号掩码/挂起信号全保留; 内存图/栈/定时器清零——这是 task_struct 分类设计的直接结果。
- /proc 是伪文件系统:读的瞬间从内核数据结构生成;self 指向
读者自己;stat 一行 52 字段,ps 全靠它;
%*d跳过不显示的字段。 - proc 里的时间字段单位是 tick——除 sysconf(_SC_CLK_TCK); start_time 要加 /proc/stat 的 btime 才是日历时间。
10. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 进程树与 pstree | Chapter 10: Process Fundamentals | text/16-ch10-chapter-10-process-fundamentals.txt:89(搜「pstree」) |
| 进程组实验与 kill -PGID | 同上 | :137(搜「ps -opid,ppid,pgrp,cmd」) · :158(搜「-16779」) |
| setpgid 变体 | 同上 | :182(搜「setpgid」) |
| 会话与 setsid/daemon | 同上 | :236(搜「daemon」) |
| 前后台、SIGTTIN、SIGHUP/nohup | 同上 | :287(搜「SIGTTIN」) · :297(搜「SIGHUP」) |
| 可执行文件内容清单 | 同上 | :331(搜「text」) |
| ELF 四类文件与双视图 | 同上 | :383(搜「Relocatable file」) · :434(搜「linking view」) |
| readelf 输出与节段映射 | 同上 | :554(搜「Magic」) · :613(搜「.init .plt」) |
| print_elfphdr 项目 | 同上 | :728(搜「typedef struct」) · :778(搜「INTERP」) · :878(搜「13 program headers」) |
| 内存五区与 ASLR | 同上 | :904(搜「five different regions」) · :987(搜「(ASLR) was introduced」) |
| displayvm 地址表 | 同上 | :1159(搜「7FFE101EA268」) · :1187(搜「0x100 bytes」) · :1192(搜「46TB」) |
| 堆与 sbrk/malloc | 同上 | :1048(搜「sbrk(0)」) · :1061(搜「malloc()」) |
| task_struct 与子结构 | 同上 | :1243(搜「300 members」) · :1256(搜「kernel stack」) |
| exec 时保留的元数据 | 同上 | :1304(搜「retains」) |
| list_head 多链表 | 同上 | :1328(搜「struct list_head」) |
| /proc 机制与 self | 同上 | :1440(搜「pseudofilesystem」) · :1449(搜「proc_read」) · :1456(搜「self」) |
| /proc 编号目录常用文件 | 同上 | :1469(搜「cmdline」) |
| ancestors 项目 | 同上 | :1509(搜「getparentid」) |
| spl_ps 与 parse_buf/%*d | 同上 | :1582(搜「GNU implementation」) · :1676(搜「parse_buf」) · :1695(搜「assignment-suppression」) |
| HZ 与 btime 换算 | 同上 | :1784(搜「get_hertz」) · :1833(搜「btime」) |
-
出处:「Chapter 10: Process Fundamentals」第 75–96 段(
text/16-ch10-chapter-10-process-fundamentals.txt:89,搜「pstree」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 98–159 段(
text/16-ch10-chapter-10-process-fundamentals.txt:137,搜「ps -opid,ppid,pgrp,cmd」) · :158(搜「-16779」)。 ↩↩ -
出处:「Chapter 10: Process Fundamentals」第 168–209 段(
text/16-ch10-chapter-10-process-fundamentals.txt:182,搜「setpgid」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 259–311 段(
text/16-ch10-chapter-10-process-fundamentals.txt:287,搜「SIGTTIN」) · :297(搜「SIGHUP」)。 ↩↩ -
出处:「Chapter 10: Process Fundamentals」第 211–257 段(
text/16-ch10-chapter-10-process-fundamentals.txt:236,搜「daemon」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 328–363 段(
text/16-ch10-chapter-10-process-fundamentals.txt:331,搜「text」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 365–403 段(
text/16-ch10-chapter-10-process-fundamentals.txt:383,搜「Relocatable file」)。 ↩↩ -
出处:「Chapter 10: Process Fundamentals」第 405–466 段(
text/16-ch10-chapter-10-process-fundamentals.txt:434,搜「linking view」)。 ↩↩ -
出处:「Chapter 10: Process Fundamentals」第 609–667 段(
text/16-ch10-chapter-10-process-fundamentals.txt:613,搜「.init .plt」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 549–590 段(
text/16-ch10-chapter-10-process-fundamentals.txt:554,搜「Magic」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 701–838 段(
text/16-ch10-chapter-10-process-fundamentals.txt:728,搜「typedef struct」) · :778(搜「INTERP」)。 ↩↩↩ -
出处:「Chapter 10: Process Fundamentals」第 840–894 段(
text/16-ch10-chapter-10-process-fundamentals.txt:878,搜「13 program headers」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 896–993 段(
text/16-ch10-chapter-10-process-fundamentals.txt:904,搜「five different regions」)。 ↩↩ -
出处:「Chapter 10: Process Fundamentals」第 982–992 段(
text/16-ch10-chapter-10-process-fundamentals.txt:987,搜「(ASLR) was introduced」)。 ↩↩ -
出处:「Chapter 10: Process Fundamentals」第 1189–1201 段(
text/16-ch10-chapter-10-process-fundamentals.txt:1192,搜「46TB」)。 ↩↩ -
出处:「Chapter 10: Process Fundamentals」第 1023–1063 段(
text/16-ch10-chapter-10-process-fundamentals.txt:1048,搜「sbrk(0)」) · :1061(搜「malloc()」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 1240–1289 段(
text/16-ch10-chapter-10-process-fundamentals.txt:1243,搜「300 members」) · :1256(搜「kernel stack」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 1292–1320 段(
text/16-ch10-chapter-10-process-fundamentals.txt:1304,搜「retains」)。 ↩↩ -
出处:「Chapter 10: Process Fundamentals」第 1321–1344 段(
text/16-ch10-chapter-10-process-fundamentals.txt:1328,搜「struct list_head」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 1357–1454 段(
text/16-ch10-chapter-10-process-fundamentals.txt:1440,搜「pseudofilesystem」) · :1449(搜「proc_read」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 1467–1486 段(
text/16-ch10-chapter-10-process-fundamentals.txt:1469,搜「cmdline」)。 ↩↩ -
出处:「Chapter 10: Process Fundamentals」第 1455–1465 段(
text/16-ch10-chapter-10-process-fundamentals.txt:1456,搜「self」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 1490–1566 段(
text/16-ch10-chapter-10-process-fundamentals.txt:1509,搜「getparentid」)。 ↩ -
出处:「Chapter 10: Process Fundamentals」第 1595–1698 段(
text/16-ch10-chapter-10-process-fundamentals.txt:1676,搜「parse_buf」) · :1695(搜「assignment-suppression」)。 ↩↩ -
出处:「Chapter 10: Process Fundamentals」第 1578–1587 段(
text/16-ch10-chapter-10-process-fundamentals.txt:1582,搜「GNU implementation」)。 ↩↩↩