理论讲完,最后一章做一次「解剖实验」:挑一个活的进程,用 Linux 的手术刀(/proc 目录)把它从头到尾切开,验证前面九章的每一个概念不是抽象名词,而是这台机器上实实在在可观测的东西。这一章也是操作系统的「实验总复习」,做完它,再去学容器、虚拟化(本站的 Linux/KVM/Docker 课程)时,你会发现那些新概念只是本章零件的重新组合。
起一个睡眠中的进程,拿到 PID(假设 21453),它的全部档案就在 /proc/21453/ 下。挑几份最重要的病历看:
cat /proc/21453/status | head -8
# Name: sleep
# State: S (sleeping) ← 第 2 章的五状态模型,活的
# Pid: 21453
# PPid: 19402 ← 父进程,进程树的证据
# Threads: 1 ← 内核级线程数
cat /proc/21453/maps
# 00400000-0040b000 r--p ... /usr/bin/sleep ← 代码段(只读)
# 0040b000-0040c000 rw-p ... /usr/bin/sleep ← 数据段
# 7f8a...-7f8b... r--p ... /lib/x86_64/libc.so.6 ← 共享库映射
# 7ffd3...-7ffd4... rw-p [stack] ← 栈,高地址,向下长
maps 这张图就是第 6 章虚拟内存的实景:每个虚拟地址区间,权限位(可读 r、可写 w、可执行 x)、映射来源(文件还是匿名)一目了然。几个直观结论:代码段永远只读可执行(权限位在 MMU 那里硬件强制,计组第 8 章);栈在最高处向下长,堆在中间向上长,两者之间的大片空洞是「还没用的地址空间」;libc 是好几个进程共享的同一份映射(共享库的物理机制)。
ls -l /proc/21453/fd/
# 0 -> /dev/pts/3 ← 标准输入,连着你的终端
# 1 -> /dev/pts/3 ← 标准输出
# 2 -> /dev/pts/3 ← 标准错误
第 8 章说「一切皆文件」,fd 目录就是证据:进程的每个打开文件是一个文件描述符链接,lsof -p 21453 看到的就是这些。运维排查「文件被谁占着」(第 8 章删了文件空间不释放那个谜题),全靠它。
| Linux | Windows | |
|---|---|---|
| 内核 | 宏内核 + 可加载模块 | 混合内核(NT) |
| 可执行格式 | ELF | PE(.exe) |
| 文件系统 | ext4/xfs/btrfs | NTFS/ReFS |
| 权限模型 | uid/gid + rwx 位 + ACL | ACL + SID,细到每条 |
| 设备抽象 | 文件(/dev、/proc) | 对象管理器,部分文件化 |
| 进程 API | fork + exec(组合拳) | CreateProcess(一步到位) |
| 源码 | 开放(GPL) | 闭源 |
两派的选择各有道理,fork+exec 的「先复制再替换」看着绕,却让「复制自己再干活」(fork 出 worker)和「换身程序」(exec)成了两个可独立组合的原语,shell 的管道、Redis 的快照都白嫖这个组合性;CreateProcess 一步到位,简单直接,但想「复制一个运行中的自己」反而要绕。我自己的体会:这两条设计路线的差异,比界面上那点区别更能解释两个生态的气质。
C 指针课第 2 章画过「进程内存布局」,当时是示意图,现在用程序把它量出来:
#include <stdio.h>
#include <stdlib.h>
int g = 1; // 已初始化数据段
int main(void) {
static int s = 2; // 也在数据段
char *lit = "hello"; // lit 在栈,字面量在只读段
int local = 3; // 栈
int *p = malloc(8); // 堆
printf("data=%p rodata=%p stack=%p heap=%p\n", &g, lit, &local, p);
return 0;
}
某次运行的真实输出:
data=0x5589c2e81020 rodata=0x5589c2e7e008 stack=0x7ffd9c1b4544 heap=0x5589c3a3c2a0
按地址高低排:只读段(最低,和代码段挨着)→ 数据段 → 堆(向高处长)→ 栈(最高,向低处长)。自己跑一遍,再对照 cat /proc/self/maps,教科书上的五段图和真机输出严丝合缝,这种「理论落地图成功」的瞬间是这门课最好的复习。堆在数据段之上隔了十几 GB?那是 ASLR(地址空间随机化)在捣乱,安全机制故意把各段基址随机偏移,让漏洞利用猜不到地址。关掉它(setarch -R)再跑,地址就紧挨着了。
最后把镜头拉远。本章的进程知识加上第 6 章的页表,再补一个新概念 namespace(让进程只「看见」自己这组的 PID、网络、挂载点,造成隔离假象),你就有了容器的全部原料:Docker 容器 = 受 namespace 限制 + 受 cgroup 限额的普通进程,里面跑着程序,外面共用一台 Linux 内核。没有魔法,没有微型操作系统,只是「可见性」和「配额」两项 OS 原生能力的组合拳。这也是为什么容器离不开 Linux(每家 OS 的内核机制不同),以及为什么虚拟机(第 5 章 KVM)和容器不是竞争关系:一个虚拟硬件,一个共享内核。
free 的 used 判断内存紧张(第 7、8 章都提过):要看 available,缓存占了的大头是可回收的。kill -9 当万能钥匙。SIGKILL 不给进程收尾机会,数据库被 -9 后要靠日志重放恢复(WAL 的价值就在这);常规重启先 SIGTERM 让它善终,-9 是最后的手段。/proc/sys/*)可写,经典的 echo 1 > /proc/sys/net/ipv4/ip_forward 打开转发,sysctl 就是它的包装。setarch -R ./a.out 关掉再跑,验证地址固定。ps aux 里一个进程的 VSZ 远大于 RSS 的现象,用第 6 章(虚拟地址空间)和 malloc(按需缺页)的知识各答一句。ps 里看得见吗?用本章的 namespace 概念推理,再开个 Docker 实验验证(没有环境就看文档)。这道题是通往 Linux/KVM/Docker 课程的桥。