一、 fork 函数的奇迹
在 Linux 中,通过 fork() 系统调用创建子进程。它最神奇的地方在于**"一次调用,两次返回"**:
-
在父进程中,返回子进程的 PID(>0)。
-
在子进程中,返回 0。
-
失败返回 <0。
因此在代码中,我们通常用
if(ret == 0) { // 子进程 } else { // 父进程 }来分流。
二、 虚拟地址与写时拷贝
在实战中,我发现一个诡异现象:父子进程打印同一个全局变量的地址是一模一样的,但值却不同。
这说明我们在 C 语言中打印的地址全都是虚拟地址 。操作系统通过页表将虚拟地址映射到物理地址。父子进程初始共享物理内存,当子进程尝试修改数据时,系统会触发"写时拷贝"(Copy-on-Write)机制,为子进程分配新的物理内存,从而保证了进程的独立性。
三、 僵尸进程(Zombie)
-
成因 :子进程退出,但父进程未使用
wait()或waitpid()读取其退出状态,导致子进程的 PCB 一直保留在内核中,状态变为Z。 -
危害:PCB 会占用内存空间,大量僵尸进程会导致内存泄漏,甚至拖垮系统。
-
解决 :父进程应调用
wait()回收,或者杀死父进程让僵尸进程变成孤儿被 init 接管。
四、 孤儿进程(Orphan)
-
成因:父进程先退出了,而子进程还在运行。
-
结局 :孤儿进程会被系统的 1号进程(init/systemd) 领养,由 1号进程负责它们的回收,因此孤儿进程没有危害。
自我实战操作
实战 1:体验 fork 的双返回值与数据私有
bash
cd ~/linux_study/proc_test
vim fork_test.c
(输入以下代码,保存退出)
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0; // 全局变量
int main() {
pid_t id = fork(); // 创建子进程
if (id < 0) {
perror("fork error");
return 1;
} else if (id == 0) {
// 只有子进程能进这里
g_val = 100; // 子进程修改全局变量
printf("我是子进程,PID: %d, 我爸爸是: %d, g_val: %d, 地址: %p\n", getpid(), getppid(), g_val, &g_val);
} else {
// 只有父进程能进这里
sleep(1); // 等子进程先跑完
printf("我是父进程,PID: %d, g_val: %d, 地址: %p\n", getpid(), g_val, &g_val);
}
return 0;
}
编译运行:
bash
gcc fork_test.c -o fork_test
./fork_test

同一个变量 g_val,父子进程打印出来的内存地址一模一样(比如都是 0x...),但值却不一样(父进程是 0,子进程是 100)。
思考:这说明什么?说明 %p 打印出来的根本不是你电脑真实的物理内存地址,而是"虚拟地址"!
实战 2:亲手制造一个"僵尸进程"
bash
vim zombie_test.c
(输入以下代码,保存退出)
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t id = fork();
if (id == 0) {
// 子进程只活 3 秒
printf("我是子进程[%d],我要死了...\n", getpid());
sleep(3);
exit(0);
} else {
// 父进程活 30 秒,且一直不调用 wait() 回收子进程
printf("我是父进程[%d],我假装没看见子进程死掉...\n", getpid());
sleep(30);
}
return 0;
}
操作步骤:
-
编译运行:
gcc zombie_test.c -o zombie_test && ./zombie_test -
马上新开一个终端,敲下面这个命令监控:
bash
while true; do ps aux | grep zombie_test | grep -v grep; sleep 1; echo "---"; done

你会看到,3秒钟后,子进程的状态会变成 Z (或者 Z+),这就叫僵尸进程!它会一直在那里挂着,直到 30 秒后父进程结束,它才被系统接管回收。