用眼睛看进程
目标
把操作系统书中读到的内容**亲手实现一遍。**僵尸进程、孤儿进程、虚拟内存、 打开的文件——全都可以在这个 Pod 里用几行代码创建出来。
这里学到的知识,以后排查故障时会直接用上。‘为什么磁盘空间没有释放?’、 ‘为什么占用了这么多内存?’、‘为什么 Pod 终止不了?’的答案全都在这里。
查看位置
Linux 会以文件的形式展示内核状态。
ps -eo pid,ppid,stat,comm # 프로세스 목록
ls /proc/<PID>/task # 그 프로세스의 스레드들
ls -l /proc/<PID>/fd # 열어 둔 파일들
cat /proc/<PID>/status # 메모리를 포함한 상태 전부
/proc 不是真正的磁盘,而是内核生成的状态视图。
在后台启动时
如果看不到 Python 的输出,说明漏掉了 flush=True。
print(os.getpid(), flush=True)
步骤
- 进程谱系 →
01-tree.txt - 线程 →
02-threads.txt - 僵尸进程 →
03-zombie.txt - 孤儿进程与 PID 1 →
04-orphan.txt - VSZ 与 RSS →
05-memory.txt - 已删除的文件 →
06-deleted.txt - TERM 与 KILL →
07-signal.txt - 总结 →
08-notes.md
提示
第 5 步和第 6 步必须把两个数值都记录下来,因为前后对比就是这两步的 全部内容。
谁创建了谁
提取当前 Pod 中运行进程的父子关系并保存到 01-tree.txt。同时写下当前 shell 的 PID 及其父进程。
ps -eo pid,ppid,stat,comm。当前 shell 用 echo $$ 查看,其父进程用 ps -o ppid= -p $$ 查看。
**每个进程都有父进程。**父进程通过 fork 创建子进程,子进程结束后,父进程通过 wait 回收其结果。这两句话就是接下来三个步骤的全部原理。
线程在哪里
启动一个创建 4 个线程的程序,展示它在 ps 中看起来只有一个进程、实际上却包含多个任务,并把结果保存到 02-threads.txt。
使用 Python 时,创建 4 个 threading.Thread 即可。用 print(os.getpid(), flush=True) 输出 PID(如果省略 flush=True,将看不到输出)。
通过 ls /proc/<PID>/task | wc -l 验证——主线程 1 个 + 线程 4 个 = 5。在 Linux 中,线程只不过是共享内存的任务,从内核的角度看,它与进程并没有太大区别。
创建僵尸进程
制造子进程已经结束、父进程却没有回收它的状态,并将它在 ps 中显示为 Z 的证据保存到 03-zombie.txt。
用 os.fork() 创建子进程,让子进程执行 os._exit(0),父进程不调用 wait() 并保持休眠即可。
僵尸进程不占用内存。它只是一个仅保留进程已结束这一事实及退出码的位置。但它仍占用 PID,积累过多后就无法创建新进程。
当父进程先退出时
让父进程先于子进程退出,并在 04-orphan.txt 中记录该子进程的 PPID 变成了什么。同时记录这个 Pod 的 PID 1 是什么。
用 ps -o ppid= -p <자식PID> 查看。失去父进程的进程会被移交给 PID 1(重新指定父进程)。
然后查看 ps -o comm= -p 1。容器中的 PID 1 通常是你的应用程序,而不是 init。如果它不是 init,就不会回收移交给它的僵尸进程——这正是容器中僵尸进程不断积累的原因,也是使用 --init 或 tini 的原因。
预留的内存与实际使用的内存
启动一个只预留 300MB、但实际仅访问其中 80MB 的程序,记录 VSZ 与 RSS 有何不同,并把结果保存到 05-memory.txt。
使用 mmap.mmap(-1, 300*1024*1024) 预留内存,并把访问前后的 ps -o vsz=,rss= -p <PID> 都记录下来。
不能使用 bytearray(300*1024*1024)——Python 会在填充 0 时把整块内存都访问一遍。
VSZ 表示预留的地址空间,RSS 表示实际映射到物理内存的部分。预留 300MB 时,RSS 甚至不到 10MB。
文件删了,空间却没有释放
创建一个 64MB 的文件,在文件仍然处于打开状态时将其删除;用 df 展示空间尚未释放,再展示终止进程后空间恢复,并把全过程保存到 06-deleted.txt。
用 dd if=/dev/zero of=big.bin bs=1M count=64 创建文件,再用 Python open() 它,执行 os.remove() 后保持休眠即可。
证据就在这里——ls -l /proc/<PID>/fd/ 中仍会显示 big.bin (deleted)。
**删除文件名和删除数据是两回事。**只有最后一个引用(无论是文件名还是打开的 fd)消失后,数据块才会被归还。删除日志后磁盘空间仍未释放,原因正是如此。
TERM 与 KILL 的区别
编写一个接收并处理 SIGTERM、但不会退出的程序,在 07-signal.txt 中记录它能在 TERM 后继续存活、却会被 KILL 终止。
signal.signal(signal.SIGTERM, 핸들러)。先执行 kill -TERM <PID>,再用 kill -0 <PID> 验证(仍存活则成功),然后执行 kill -KILL <PID>。
**TERM 是请求,KILL 是通知。程序可以接收 TERM 并获得清理的机会,而 KILL 会由内核直接将其移除——因此无法执行收尾工作。**这就是 Kubernetes 在终止时先发送 TERM,等待 terminationGracePeriodSeconds 后再发送 KILL 的原因。
总结三点
在 08-notes.md 中至少写三行:什么是僵尸进程、它为何产生,VSZ 与 RSS 的区别,以及已删除文件的空间何时会释放。
正文中必须包含 좀비、RSS、참조。