跳转至

进程基础 — 进程树、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。

思路就是把「路标」走一遍:读 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),一次运行看清五段布局。

  1. 进程树以 init(PID 1)为根;管道进程同组,组长 PID=PGID, CTRL-C 全组收;kill -PGID 按组发。
  2. 会话 = 登录创建的全部进程组;前台组才能读终端;后台读收 SIGTTIN;SIGHUP 杀全会话——nohup 是后台长寿的门票。
  3. setsid = 新会话 + 新组 + 无控制终端 = daemon 的第一步。
  4. 可执行文件 = text + 入口点 + 重定位/符号/字符串表 + 数据 + 动态链接信息;ELF 链接视图按节、执行视图按段,段=相邻节的组。
  5. readelf -h/-l/-S/-s 看 ELF;程序头表位置=e_phoff, 大小=e_phentsize×e_phnum。
  6. 虚拟内存五区:text(只读共享)/data/BSS(不占文件)/heap (往上)/stack(往下,装 argv/envp);ASLR 打乱起点、保留相对 位置。
  7. etext/edata/end 是三段尾的路标;sbrk(0) 看 program break, 但分配内存用 malloc。
  8. exec 换程序不换身份:PID/目录/umask/信号掩码/挂起信号全保留; 内存图/栈/定时器清零——这是 task_struct 分类设计的直接结果。
  9. /proc 是伪文件系统:读的瞬间从内核数据结构生成;self 指向 读者自己;stat 一行 52 字段,ps 全靠它;%*d 跳过不显示的字段。
  10. 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」)

  1. 出处:「Chapter 10: Process Fundamentals」第 75–96 段(text/16-ch10-chapter-10-process-fundamentals.txt:89,搜「pstree」)。 ↩

  2. 出处:「Chapter 10: Process Fundamentals」第 98–159 段(text/16-ch10-chapter-10-process-fundamentals.txt:137,搜「ps -opid,ppid,pgrp,cmd」) · :158(搜「-16779」)。 ↩↩

  3. 出处:「Chapter 10: Process Fundamentals」第 168–209 段(text/16-ch10-chapter-10-process-fundamentals.txt:182,搜「setpgid」)。 ↩

  4. 出处:「Chapter 10: Process Fundamentals」第 259–311 段(text/16-ch10-chapter-10-process-fundamentals.txt:287,搜「SIGTTIN」) · :297(搜「SIGHUP」)。 ↩↩

  5. 出处:「Chapter 10: Process Fundamentals」第 211–257 段(text/16-ch10-chapter-10-process-fundamentals.txt:236,搜「daemon」)。 ↩

  6. 出处:「Chapter 10: Process Fundamentals」第 328–363 段(text/16-ch10-chapter-10-process-fundamentals.txt:331,搜「text」)。 ↩

  7. 出处:「Chapter 10: Process Fundamentals」第 365–403 段(text/16-ch10-chapter-10-process-fundamentals.txt:383,搜「Relocatable file」)。 ↩↩

  8. 出处:「Chapter 10: Process Fundamentals」第 405–466 段(text/16-ch10-chapter-10-process-fundamentals.txt:434,搜「linking view」)。 ↩↩

  9. 出处:「Chapter 10: Process Fundamentals」第 609–667 段(text/16-ch10-chapter-10-process-fundamentals.txt:613,搜「.init .plt」)。 ↩

  10. 出处:「Chapter 10: Process Fundamentals」第 549–590 段(text/16-ch10-chapter-10-process-fundamentals.txt:554,搜「Magic」)。 ↩

  11. 出处:「Chapter 10: Process Fundamentals」第 701–838 段(text/16-ch10-chapter-10-process-fundamentals.txt:728,搜「typedef struct」) · :778(搜「INTERP」)。 ↩↩↩

  12. 出处:「Chapter 10: Process Fundamentals」第 840–894 段(text/16-ch10-chapter-10-process-fundamentals.txt:878,搜「13 program headers」)。 ↩

  13. 出处:「Chapter 10: Process Fundamentals」第 896–993 段(text/16-ch10-chapter-10-process-fundamentals.txt:904,搜「five different regions」)。 ↩↩

  14. 出处:「Chapter 10: Process Fundamentals」第 982–992 段(text/16-ch10-chapter-10-process-fundamentals.txt:987,搜「(ASLR) was introduced」)。 ↩↩

  15. 出处:「Chapter 10: Process Fundamentals」第 1189–1201 段(text/16-ch10-chapter-10-process-fundamentals.txt:1192,搜「46TB」)。 ↩↩

  16. 出处:「Chapter 10: Process Fundamentals」第 1023–1063 段(text/16-ch10-chapter-10-process-fundamentals.txt:1048,搜「sbrk(0)」) · :1061(搜「malloc()」)。 ↩

  17. 出处:「Chapter 10: Process Fundamentals」第 1240–1289 段(text/16-ch10-chapter-10-process-fundamentals.txt:1243,搜「300 members」) · :1256(搜「kernel stack」)。 ↩

  18. 出处:「Chapter 10: Process Fundamentals」第 1292–1320 段(text/16-ch10-chapter-10-process-fundamentals.txt:1304,搜「retains」)。 ↩↩

  19. 出处:「Chapter 10: Process Fundamentals」第 1321–1344 段(text/16-ch10-chapter-10-process-fundamentals.txt:1328,搜「struct list_head」)。 ↩

  20. 出处:「Chapter 10: Process Fundamentals」第 1357–1454 段(text/16-ch10-chapter-10-process-fundamentals.txt:1440,搜「pseudofilesystem」) · :1449(搜「proc_read」)。 ↩

  21. 出处:「Chapter 10: Process Fundamentals」第 1467–1486 段(text/16-ch10-chapter-10-process-fundamentals.txt:1469,搜「cmdline」)。 ↩↩

  22. 出处:「Chapter 10: Process Fundamentals」第 1455–1465 段(text/16-ch10-chapter-10-process-fundamentals.txt:1456,搜「self」)。 ↩

  23. 出处:「Chapter 10: Process Fundamentals」第 1490–1566 段(text/16-ch10-chapter-10-process-fundamentals.txt:1509,搜「getparentid」)。 ↩

  24. 出处:「Chapter 10: Process Fundamentals」第 1595–1698 段(text/16-ch10-chapter-10-process-fundamentals.txt:1676,搜「parse_buf」) · :1695(搜「assignment-suppression」)。 ↩↩

  25. 出处:「Chapter 10: Process Fundamentals」第 1578–1587 段(text/16-ch10-chapter-10-process-fundamentals.txt:1582,搜「GNU implementation」)。 ↩↩↩