本文是小编巩固自身而作,如有错误,欢迎指出!
目录
[1. R --- Running / Runnable(运行 / 就绪)](#1. R — Running / Runnable(运行 / 就绪))
[2. S --- Interruptible Sleep(可中断睡眠)](#2. S — Interruptible Sleep(可中断睡眠))
[3. D --- Uninterruptible Sleep(不可中断睡眠)](#3. D — Uninterruptible Sleep(不可中断睡眠))
[4. T --- Stopped(暂停)](#4. T — Stopped(暂停))
[5. Z --- Zombie(僵尸进程)](#5. Z — Zombie(僵尸进程))
一、fork
在编程领域,fork 是一个核心且高频出现的概念,最常见于 Unix/Linux 系统编程.
1.fork的原理
fork() 是 Unix/Linux 系统提供的一个系统调用(函数),作用是复制当前进程,创建一个全新的子进程。
- 调用
fork()后,操作系统会为子进程复制父进程的内存空间、代码、数据、文件描述符等资源。 - 一次调用,两次返回:
- 父进程中,
fork()返回子进程的 PID(进程 ID); - 子进程中,
fork()返回 0; - 若创建失败(如资源不足),返回 -1。
- 父进程中,
2.fork的示例
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
// 调用fork创建子进程
pid_t pid = fork();
if (pid < 0) {
// 失败场景
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子进程执行逻辑
printf("我是子进程,PID:%d,父进程PID:%d\n", getpid(), getppid());
// 子进程执行完退出
return 0;
} else {
// 父进程执行逻辑
printf("我是父进程,PID:%d,创建的子进程PID:%d\n", getpid(), pid);
// 等待子进程结束,避免僵尸进程
wait(NULL);
printf("子进程已结束\n");
}
return 0;
}

我们可以看到子进程的ppid和父进程的pid是一样的。说明fork的作用是在原有的进程基础上创建一个子进程,而不是在总的进程下创建一个子进程。
而我们自己创建的进程的父进程又是什么呢?很简单,在所有我们不曾用fork创建新的子进程的情况下,原本创建的进程的父进程都是bash。而bash也就是我们用fork创建进程的爷爷进程。
3.fork的执行过程
我们在看上述代码的时候,不免有一个疑问?为什么一个fork函数可以返回两个值?
不是
fork()函数本身返回了两个值,而是操作系统让fork()调用在父进程和子进程中分别返回了不同的值。
fork() 的本质是「复制当前进程」,整个过程可以分成 3 步:
- 你在父进程中调用
fork(),操作系统接收到这个请求; - 操作系统会完整复制父进程的所有资源(代码、内存、文件描述符等),创建一个全新的子进程;
- 此时系统中存在两个独立的进程(父 + 子),它们会从
fork()调用的位置继续往下执行代码。
为什么需要「不同的返回值」?
父进程和子进程执行的是同一份代码,如果 fork() 返回的值相同,这两个进程就会执行完全一样的逻辑 ------ 但实际开发中,我们往往需要让父子进程做不同的事(比如父进程监听端口,子进程处理请求)。
因此,操作系统设计了这个「差异化返回」的机制,目的是:让程序员能通过返回值区分「当前代码跑在父进程还是子进程」,从而编写不同的逻辑。


二、进程状态
1.操作系统的进程状态

在操作系统中一般我们认为有三种状态
-
**运行(Running)**正在用 CPU。
-
**就绪(Ready)**除了 CPU,别的资源都有,等 CPU。
-
阻塞(Blocked/Waiting) 等事件(I/O、信号),就算 CPU 空着也不能运行。
而在特殊情况下,我们还有一种状态就是挂起
挂起 = 进程被调到外存(磁盘),不在内存里了原因:内存不够、进程太久不用、管理员手动挂起。
挂起分两种:
(1)就绪挂起(Ready Suspend)
- 本来是就绪态
- 被换到磁盘
- 只要调回内存 → 就变成就绪
(2)阻塞挂起(Blocked Suspend)
- 本来是阻塞态
- 被换到磁盘
- 等的事件来了 → 变成 就绪挂起
2.linux中的进程状态
bash
//linux源代码如下
static const char * const task_state_array[] = {
"R (running)" //运行
"S (sleeping)" //睡眠
"D (disk sleep)" //深度睡眠
"T (stopped)" //停止
"t (tracing stop)"//追踪停止
"X (dead)" //死亡
"Z (zombie)", //僵尸
};
1. R --- Running / Runnable(运行 / 就绪)
- 正在 CPU 上运行 or 在就绪队列排队等着运行
- 只有 R 状态的进程才会被 CPU 调度
2. S --- Interruptible Sleep(可中断睡眠)
- 最常见状态
- 进程在等待事件:网络、键盘、信号、sleep、锁等
- 可以被信号唤醒、杀死
3. D --- Uninterruptible Sleep(不可中断睡眠)
- 一般在等待 I/O:磁盘读写
- 不能被信号打断,不能被杀掉
- 大量 D 状态 = 磁盘 I/O 卡住、系统很卡
4. T --- Stopped(暂停)
- 被暂停执行
- 比如:
Ctrl+Z、被调试器(gdb)暂停
5. Z --- Zombie(僵尸进程)
- 进程已经结束、退出
- 但父进程没有调用
wait()回收它的 PCB 信息 - 僵尸进程占进程号,不占内存,太多会炸系统
状态示例
比如我们这里写一个简单的死循环
cpp
#include <stdio.h>
int main()
{
while(1)
{}
return 0;
}

我们可以看到这样就是运行状态了。
但如果我们在循环中加入一个printf呢?

我们可以看到在这种情况下我们的进程状态居然变成了S
原因就在于终端输出的阻塞特性
printf向终端(pts/0)输出内容时,会触发终端 I/O 缓冲区操作 ,进程需要等待终端内核缓冲区就绪,这个等待过程会让进程短暂进入S(可中断睡眠)状态;- 虽然没有
sleep,但printf与终端的交互是频繁的 "短暂运行(R)→ 短暂睡眠(S)" 循环,ps采样时大概率捕捉到的是S状态(而非瞬时的R状态)。
僵尸进程
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
// 1. 创建子进程:fork() 是创建进程的系统调用
// 返回值 pid_t 是进程ID类型,不同返回值区分父子进程
pid_t id = fork();
// 2. 错误处理:fork() 失败时返回 -1
if(id < 0){
// perror 打印错误原因(如内存不足、进程数超限等)
perror("fork");
// 非0返回表示程序异常退出
return 1;
}
// 3. 父进程逻辑:fork() 给父进程返回子进程的PID(大于0)
else if(id > 0){
// 打印父进程PID,getpid() 获取当前进程的ID
printf("parent[%d] is sleeping...\n", getpid());
// 父进程睡眠30秒:这段时间父进程不执行任何操作,也不回收子进程
// 核心:父进程睡眠期间,子进程退出后会变成僵尸进程
sleep(30);
}
// 4. 子进程逻辑:fork() 给子进程返回 0
else{
// 打印子进程PID
printf("child[%d] is begin Z...\n", getpid());
// 子进程睡眠5秒:模拟子进程的业务执行时间
sleep(5);
// 子进程正常退出:EXIT_SUCCESS 等价于 0,表示退出状态正常
// 子进程退出后,父进程还在sleep(30),未调用wait()回收资源 → 子进程变僵尸
exit(EXIT_SUCCESS);
}
// 父进程睡眠30秒后,程序正常结束(此时系统会自动回收子进程)
return 0;
}
僵尸进程(Zombie)= 子进程已退出(运行完毕 / 异常终止) + 父进程未调用 wait()/waitpid() 回收子进程的「退出状态和资源」。
僵尸进程的关键特征
✅ 占用 PID,但不占用 CPU / 内存(只占极少量内核空间存储 PCB);
✅ 无法被
kill -9杀死(因为进程已经退出,信号无接收者);✅ 只有两种清理方式:
- 父进程调用
wait()/waitpid()主动回收;- 父进程退出,由「init 进程(PID=1)」接管并回收。
孤儿进程
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork(); // 创建父子进程
if(id < 0){
perror("fork");
return 1;
}
// 父进程逻辑:先退出,制造孤儿进程
else if(id > 0){
printf("父进程[PID: %d] 即将退出,子进程[PID: %d] 变成孤儿\n", getpid(), id);
sleep(1); // 留时间打印信息
exit(EXIT_SUCCESS); // 父进程主动退出
}
// 子进程逻辑:持续运行,成为孤儿
else{
printf("子进程[PID: %d] 开始运行,父进程退出后,我的新父进程会变成 1\n", getpid());
// 子进程持续运行10秒,方便你查看状态
for(int i = 1; i <= 10; i++){
// getppid():获取当前进程的父进程PID
printf("子进程[PID: %d] - 第%d秒,父进程PID: %d\n", getpid(), i, getppid());
sleep(1);
}
printf("子进程[PID: %d] 退出\n", getpid());
return 0;
}
}
孤儿进程 = 父进程先退出 + 子进程还在运行 → 此时子进程会被系统的「1 号 init 进程(或 systemd)」接管,成为 init 进程的 "养子"。
- init 进程的作用:系统启动后第一个进程(PID=1),专门负责收养孤儿进程,子进程退出后会被 init 进程自动回收,不会变成僵尸;
- 孤儿进程无危害:它就是一个普通的运行中进程,只是 "没了亲爹",由系统接管,资源使用和普通进程完全一致;
- 与僵尸进程的本质区别 :
- 僵尸进程是「子死父不管」;
- 孤儿进程是「父死子还在」。
本次分享就到这里结束了,后续会继续更新,感谢阅读!
