05 - Programs and processes

05 - 程序与进程

本次课开始,我们开始进行系统调用的学习...

学习程序和进程管理

学习进程管理的API

1. 虚拟化

物理计算机 - 抽象为 - 虚拟计算机

  • 程序好像独占计算机运行

1.1. 进入 “每一讲都实现一点什么” 的模式

  • 每次课都感到编程能力的增长

2. 程序 v.s. 进程

程序是状态机的静态描述

  • 描述了所有可能的程序状态

  • 程序(动态)运行起来,就形成了进程(进行中的程序)

程序在静态时,不会有任何的状态...就像在 vim 里写代码,如果没有 IDE 的检查,你并不知道你犯了上面错误...知道你运行起来

2.1. 操作系统上的进程

进程:程序的运行状态随时间的演进

  • 除了程序状态,操作系统还会保存一些额外的 (只读) 状态

  • 我们可以探索:试图获取当前进程的各种信息

利用 AI + Prompt

基本信息

  • 进程ID:使用 getpid() 函数可获取当前进程的ID,它是进程的唯一标识,示例代码为 pid_t pid = getpid(); 。

  • 父进程ID:通过 getppid() 函数获取当前进程的父进程ID,代码为 pid_t ppid = getppid(); 。

  • 进程组ID:使用 getpgid(0) 获取当前进程的进程组ID,代码为 pid_t pgid = getpgid(0); 。

  • 会话ID:调用 getsid(0) 可获取当前进程的会话ID,代码为 pid_t sid = getsid(0); 。

状态信息

  • 进程状态:通过读取 /proc/[pid]/stat 文件或使用 waitpid() 函数获取进程状态。 stat 文件中包含进程的运行状态、睡眠状态等信息,

  • 进程优先级:使用 nice() 函数获取或设置进程的优先级,代码为 int priority = nice(0); ,返回值为当前进程的优先级。

资源信息

  • 内存使用情况:通过读取 /proc/[pid]/status 文件获取进程的内存使用信息,如 VmSize 表示进程的虚拟内存大小, VmRSS 表示进程的常驻内存大小等

  • ......

......

2.2. AI 学习 操作系统 system calls

proc.c: 这个示例展示了如何获取进程的基本信息:除了使用 API (例如 getpid 是一个系统调用),Linux /proc 文件系统允许我们使用文件 API (“everything is a file”) 访问当前进程的 ID、状态信息、命令行参数和工作目录等元数据。

./proc 1 2 3 | head -n 2

可以利用管道,保存输出,再打印...

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec5/processes# ./proc
pid: 1230
ppid: 888

--- Additional Process Information ---
User ID: 0
Group ID: 0
Effective User ID: 0
Effective Group ID: 0
Max open file descriptors: 1024
Process priority: 0

--- Process Status (/proc/self/status) ---
Name:   proc
State:  R (running)
PPid:   888
Uid:    0       0       0       0
VmSize:     2680 kB

--- Command Line (/proc/self/cmdline) ---
Command line: ./proc

--- Current Directory (/proc/self/cwd) ---
Current directory: /mnt/d/CSLab/osCourse/lec5/processes

3. 啊......我们开始编程了!

3.1. 注意你的代码质量√

要重视所有的架构设计

函数名、变量名、参数设计...所有的设计如果都不重视,最后会导致...程序只能删掉重写

  • 程序既是 “人类” 的,也是 “反人类” 的

永远遵守两个规则:

  • 机器永远是对的

  • 未测代码永远是错的

3.2. 但你们不会好好测试的×

在你还没被 AI 替代的时候,好好重视编程规范

  • 你们甚至不知道有什么主流的 C 语言测试框架

    • (Unity、CppUTest、CUnit、Google Test)

    • 但看穿一切的 LLM 知道

    • 我们为大家的实验做了一个 “稍稍好用一些” 的库

      • add workspace folder...

  • 自动获得写测试用例的能力

  • 对你的程序可以进行单元测试等行为...

    • 这时候可以随便加...对原有程序行为没有任何影响...

    • 或者加上环境变量...可以输出失败原因

  • 极大降低写 Unit Test 的难度...

    • 用 AI 读 test lib 的代码...

    • 不会写 test example 可以给 AI 写

    • 主要是养成写 Unit Test 的习惯...以后遇到别的 Project 的时候

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec5/testkit# make
gcc -Wall -g -I. main.c testkit.c -o main
argv[0] = ./main

TestKit
- [PASS] test_simple (main.c:23)
- [FAIL] test_fail (main.c:27) - Assertion fail
Assertion failed: (114514 == 0x114514)
    In __tk_test_fail_27 of main.c:28
    Assertion violated
- [FAIL] test_timeout (main.c:31) - Timeout

- [FAIL] test_exit_fail (main.c:35) - Assertion fail
Assertion failed: (result->exit_status == 0)
    In __tk_test_exit_fail_35 of main.c:36
    Assertion violated
- [PASS] test_exit_pass (main.c:39)
- [FAIL] test_timeout (main.c:43) - Timeout
argv[0] = ./main
argv[1] = arg1
argv[2] = arg2
- [PASS] test_system_pass (main.c:47)
- [FAIL] test_system_segfault (main.c:53) - Segmentation fault
argv[0] = ./main
argv[1] = crash
Ready to crash
- 3/8 test cases passed.
make: [Makefile:7: test] Error 1 (ignored)

今天开始用上 it.

4. 进程(状态机)管理 API

getpid() 询问操作系统,自己的状态

4.1. 进程管理系统调用

操作系统 = 状态机的管理者

  • 进程管理 = 状态机管理

  • 比如:图形化界面管理

    • 就是绘制所有的进程,供使用者调用

    • 作为 CSer 主要是在程序内部对进场进行调用

    • 其他er 则是使用计算机,利用 UI 界面

一个直观的想法

  • 创建状态机:spawn(path, argv)

    • syscall:给他传递一些参数,之后就可以完成...

      • 文件路径 和 参数

    • 所以这是我们非实现不可的一个 API

  • 销毁状态机: _exit()

    • > fun fact: exit() 早期被占用了...所以这里要加一个 _

    • 这是一个合理的设计 (例子:Windows)

    • 今天可以花 10s 学习:Linux 和 Windows 的操作系统有什么不同?

      • Windows 和 Linux 的操作系统调用

      • 命名方式:Linux:quick and dirty || Windows:微软的 匈牙利命名法

AI 对操作系统学习 的 帮助:

(jyy: 我们的课程以 Linux 系统为主。但只要掌握基本思路并适当追问,例如追问 “还有哪些进程管理相关的 API”,我们就能迅速上手另一个系统的编程。)

UNIX 的答案

没有提供 spawn...而是给出一个反直觉的 API...

  • 复制状态机: fork()

  • 复位状态机: execve()

4.2. 创建状态机

Unix:fork()

pid_t fork(void);

现在我们已经有 “一个状态机” 了

  • 只需要 “创建状态机” 的 API 即可

  • UNIX 的答案: fork

    • 做一份状态机完整的复制 (内存、寄存器现场)

4.3. fork() 的行为

立即复制状态机

  • 包括所有状态的完整拷贝

    • 寄存器 & 每一个字节的内存

    • Caveat: 进程在操作系统里也有状态: ppid, 文件, 信号, ...

      • 小心这些状态的复制行为

      • 新的pid...这些不会直接复制...

      • 有一些直接复制的 内存 和 寄存器

    • 复制失败返回 -1

      • errno 会返回错误原因 (man fork)

如何区分两个状态机?

  • 新创建进程返回 0

  • 执行 fork 的进程返回子进程的进程号——“父子关系”

4.4. 进程树

pstree

进程的创建关系形成了进程树

  • ABC,如果 B 终止了……C 的 ppid 是什么?

    • 看似简单:我们 “往上提” 一层就行

    • 实际复杂:

      • parent 如果比 son 先结束

        • pid 会被回收?

        • 回收之后 son 的 ppid 是谁?

        • 总有一天会复用

        • 还有可能通过 ppid 进行进程间通信

      • 子进程结束会通知父进程

        • 通过 SIGCHLD 信号

        • 父进程可以捕获这个信号 (参考 testkit 的实现)

      • “往上提” 就发错人了

        • PPID 错误

          • 进程 C 的 PPID 可能会指向一个已经不存在的进程(B),或者一个被复用的 PID,这会导致进程间通信(IPC)等操作出错。

        • 资源管理问题

          • 父进程(A)可能没有准备好管理额外的子进程(C),这会导致资源管理混乱。

Form AI:

孤儿进程(orphan)和僵尸进程

  • 孤儿进程:当父进程终止时,子进程(C)会变成孤儿进程。

    • Linux 系统会自动将孤儿进程的父进程设置为 init 进程(PID 为 1)。

    • init 进程是系统启动时的第一个进程,负责管理所有孤儿进程。

  • 僵尸进程:如果子进程(C)在父进程(B)终止后仍然运行,而父进程(B)已经终止,那么父进程(B)会变成僵尸进程。

    • 僵尸进程的 PID 仍然存在,但它的资源已经被释放,只是它的状态信息仍然保留在系统中,直到子进程(C)读取这些信息为止。

如何验证这个行为?

  • 让 AI 帮我们写程序!

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec5/pstree# make
gcc -Wall -g create-tree.c -o create-tree
root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec5/pstree# ./create-tree
```mermaid
graph TD
1296 --> 1297
1297 --> 1298
1301 --> 1303
1298 --> 1301
1301 --> 1304
1302 --> 1305
1298 --> 1302
1302 --> 1306
1296 --> 1300
1300 --> 1308
1311 --> 1312
1308 --> 1311
1311 --> 1314
1297 --> 1299
1299 --> 1313
1313 --> 1315
1313 --> 1317
1309 --> 1318
1308 --> 1309
1309 --> 1319
1316 --> 1320
1299 --> 1316
1316 --> 1321
1300 --> 1307
1310 --> 1323
1307 --> 1310
1310 --> 1324
1322 --> 1325
1307 --> 1322
1322 --> 1326
```
```mermaid
graph TD
887 --> 1298
1301 --> 1303
1302 --> 1306
1301 --> 1304
887 --> 1301
887 --> 1302
1296 --> 1300
887 --> 1308
887 --> 1311
1311 --> 1314
887 --> 1317
887 --> 1299
887 --> 1309
1309 --> 1319
887 --> 1316
1316 --> 1321
887 --> 1323
887 --> 1307
1322 --> 1325
887 --> 1322
887 --> 1315
```

./create-tree > test.md结果重定向

这样就能得到一个进程树了!

4.5. Fork Bomb

1 变 2,2 变 4,第 k 层有 2^k-1 个进程

  • 很快资源会被耗光

  • 早期的一种 CS 恶作剧...

进程分裂...指数级增长...

4.6. 理解 fork(): 习题(1)

阅读程序,写出运行结果

pid_t x = fork();
pid_t y = fork();
printf("%d %d\n", x, y);

一些重要问题

  • 到底创建了几个状态机?

    • line 1:2 个 fork

    • line 2:4 个 fork

    • 父进程: x 是第一个子进程的 PID, y 是第二个子进程的 PID。

    • 第一个子进程: x 是 0, y 是第二个子进程的 PID。

    • 第二个子进程: x 是第一个子进程的 PID, y 是 0。

    • 第一个子进程的子进程: x 是 0, y 是 0。

    • 所以,最终会有四个执行路径,对应四个不同的状态机(进程)。

  • 当操作系统发生并发时,就会变得有点难...

    • 并发编程...

  • pid 分别是多少?

    • “状态机视角” 帮助我们严格理解

我的偷懒 (出期末考试题) 方法

  • 拍脑袋 → 估算难度 → 用 model checker(上节课的 mosaic 可以逃课...) 跑结果

4.7. 理解 fork(): 习题(2)

阅读程序,写出运行结果

for (int i = 0; i < 2; i++) {
    fork();
    printf("Hello\n");
}

状态机视角帮助我们严格理解程序行为

  • ./a.out

  • ./a.out | cat

    • 计算机系统里没有魔法

    • (无情执行指令的) 机器永远是对的

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec5/fork-demo# ./demo-1
1437 1438
1437 0
0 1439
0 0
root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec5/fork-demo# ./demo-2
Hello
Hello
Hello
Hello
Hello
Hello
root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec5/fork-demo# ./demo-2
Hello
Hello
Hello
Hello
Hello
Hello
root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec5/fork-demo# ./demo-2 | wc -l
8

但是为什么加入管道之后不对了?

加入重定向...缓冲区问题?

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec5/fork-demo# ./demo-2 > output.txt
root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec5/fork-demo# cat output.txt
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello

在某些情况下,终端的缓冲区可能会导致输出的行数统计不准确。例如,终端可能在某些情况下缓存了额外的输出。如何验证:尝试在不同的终端或环境中运行相同的命令,看看是否会出现相同的结果。

4.8. 复位状态机

int execve(const char *filename,
           char * const argv[], char * const envp[]);

UNIX 选择只给一个复位状态机的 API

  • 将当前进程重置成一个可执行文件描述状态机的初始状态

  • 操作系统维护的状态不变:进程号、目录、打开的文件……

    • (程序员总犯错,因此打开文件有了 O_CLOEXEC)

execve 是唯一能够 “执行程序” 的系统调用

  • 因此也是一切进程 strace 的第一个系统调用

  • 参数:(文件路径,参数)

ppid 是 继承的...

环境变量:

所有的设定,只要是你觉得很神奇的东西,大部分都在里面

  • 报错信息为什么是中文/Eng?

  • ...

4.9. execve() 设置了进程的初始状态

argc & argv: 命令行参数

  • 困扰多年的疑问得到解答:main 的参数是 execve 给的!

envp: 环境变量

  • 使用 env 命令查看

    • PATH, PWD, HOME, DISPLAY, PS1, ...

  • export: 告诉 shell 在创建子进程时设置环境变量

    • 小技巧:export TK_VERBOSE=1

程序被正确加载到内存

  • 代码、数据、PC 位于程序入口

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec5/execve-demo# ./demo
PWD=/mnt/d/CSLab/osCourse/lec5/execve-demo
HELLO=WORLD
SHLVL=0
_=/usr/bin/env

4.10. PATH环境变量

可执行文件搜索路径

  • 还记得 gcc 的 strace 结果吗?

  • 这个搜索顺序恰好是 PATH 里指定的顺序

计算机系统里没有魔法。机器永远是对的。

4.11. 摧毁状态机

void _exit(int status);

这个没有争议

  • 立即摧毁状态机,允许有一个返回值

    • 返回值可以被父进程获取

4.12. Unix 进程的生命周期

UNIX 中实现 “创建新状态机” 的方式

  • Spawn = fork + execve

  • 我们会在之后介绍这些系统调用的灵活应用

int pid = fork();
if (pid == -1) { // 错误
    perror("fork"); goto fail;
} else if (pid == 0) { // 子进程
    execve(...);
    perror("execve"); exit(EXIT_FAILURE);
} else { // 父进程
    ...
    int status;
    waitpid(pid, &status, 0); // testkit.c 中有
}

Last updated