进程基础:进程的概念、状态转换、PCB、创建与终止
大家好,我是你们的老朋友。今天咱们聊聊进程——这个操作系统里最核心的概念之一。说实话,我刚开始学的时候也觉得这玩意儿挺抽象的,不就是个正在运行的程序吗?但后来踩了不少坑才明白,进程远比你想象的要复杂。
进程到底是什么?
先说说我的理解。程序是躺在硬盘上的一堆指令,而进程是这些指令被加载到内存后,正在执行的那个"活物"。你想想看,同一个程序可以同时运行多次,比如你打开三个浏览器窗口,每个都是一个独立的进程。它们共享同一份代码,但各自有独立的数据空间。
我曾在项目中遇到过一个问题:两个进程同时写同一个日志文件,结果日志内容乱成一团。这就是典型的进程间资源竞争问题。说白了,进程是资源分配的最小单位,每个进程都有自己的地址空间、文件描述符、信号处理方式等。
核心要点:进程 = 程序 + 执行状态 + 资源集合。程序是静态的,进程是动态的。
进程的状态转换
进程不是一直跑着的,它会在几种状态之间跳来跳去。嗯,这里要注意,不同教材可能说法不太一样,但最核心的状态就三个:
- 就绪态:进程已经准备好运行,就等CPU分配时间片了。说白了就是"万事俱备,只欠CPU"。
- 运行态:进程正在CPU上执行指令。注意,单核CPU上同一时刻只能有一个进程处于运行态。
- 阻塞态:进程在等待某个事件(比如I/O完成、信号量释放),暂时无法继续执行。
为什么会发生状态转换?我举个例子:你写了一个网络爬虫,它发起HTTP请求后,如果网络延迟很高,进程就会从运行态变成阻塞态,把CPU让给其他进程。等数据回来了,再变回就绪态,等待调度器给它分配CPU。
个人经验:我曾经调试过一个死锁问题,就是因为两个进程互相等待对方释放资源,结果都卡在阻塞态出不来。排查时看进程状态表,一眼就发现了问题。
下面这张图展示了进程状态的完整转换关系:
进程控制块(PCB)
操作系统怎么管理这么多进程?靠的就是PCB。每个进程都有一个PCB,里面存着它的全部"家底"。我习惯把PCB想象成进程的身份证+档案袋。
PCB里都存了啥?我列个表给你看:
| 字段 | 说明 | 我的备注 |
|---|---|---|
| 进程ID(PID) | 唯一标识一个进程 | 就像身份证号 |
| 程序计数器 | 下一条要执行的指令地址 | 进程被切换时靠它恢复现场 |
| CPU寄存器 | 通用寄存器、栈指针等 | 上下文切换的核心数据 |
| 内存限制 | 进程能访问的内存范围 | 防止越界访问 |
| 打开文件列表 | 进程持有的文件描述符 | 我遇到过文件描述符泄漏,就是这里没清理 |
| 进程状态 | 就绪/运行/阻塞等 | 调度器主要看这个 |
注意:PCB是内核空间的数据结构,用户程序不能直接访问。我曾经试图在用户态直接修改PCB里的状态字段,结果程序直接崩溃了——这是内核才能碰的东西。
进程的创建
在Linux下,创建进程最常用的就是fork()系统调用。我刚开始用的时候觉得它挺神奇的——调用一次,返回两次。
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("我是子进程,PID=%d\n", getpid());
} else if (pid > 0) {
// 父进程
printf("我是父进程,子进程的PID=%d\n", pid);
} else {
// fork失败
perror("fork失败");
return 1;
}
return 0;
}
fork()执行后,子进程会复制父进程的PCB、地址空间、文件描述符等。但注意,这是"写时复制"——只有真正写入数据时才会复制内存页,这样效率高很多。
除了fork(),还有exec()系列函数用来加载新程序。我习惯的做法是:先用fork()创建子进程,然后在子进程里调用exec()执行新程序。这样父进程可以继续做自己的事。
避坑指南:我曾经在fork()之后忘记检查返回值,结果子进程和父进程都执行了同一段逻辑,导致数据被重复处理。记住:fork()返回后,一定要通过pid判断当前是父进程还是子进程。
进程的终止
进程终止有几种方式:
- 正常退出:main函数返回,或者调用exit()、_exit()
- 异常终止:收到信号(比如SIGKILL、SIGSEGV)
- 被其他进程杀死:通过kill()系统调用
进程终止时,内核会做几件事:释放内存、关闭打开的文件、清理PCB。但注意,子进程终止后,它的PCB不会立即被回收——它会变成"僵尸进程",直到父进程调用wait()或waitpid()来获取它的退出状态。
我曾经踩过的坑:写一个长时间运行的服务时,忘了处理子进程的退出状态,结果系统里积累了几百个僵尸进程。最后不得不重启服务才清理掉。所以,记得在父进程里调用wait(),或者用信号处理机制来回收子进程。
下面这段代码展示了如何正确等待子进程退出:
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:做一些工作然后退出
printf("子进程开始工作...\n");
sleep(2);
printf("子进程结束\n");
return 42; // 返回退出码
}
// 父进程:等待子进程结束
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("子进程正常退出,退出码=%d\n", WEXITSTATUS(status));
}
return 0;
}
嗯,进程这块内容其实挺多的,但今天咱们先把这些核心概念理清楚。记住:进程是资源分配的基本单位,PCB是它的"身份证",状态转换是调度器工作的基础。搞懂了这些,后面讲线程、同步、通信的时候就会轻松很多。
公众号:蓝海资料掘金营,微信deep3321