
观众老爷们大家好 这里是邪修KING的独家频道 本文属于系列Linux系统篇 ------操作指令 一起学Linux的小伙伴可订阅专栏: Linux系统篇
一、进程是什么?
1. 先分清:程序 vs 进程
很多人容易把二者搞混,其实区别非常明确:
- 程序(Program) :静态的 ,就是存放在磁盘上的可执行文件(比如我们之前编译生成的
mycmd、系统里的ls命令都属于程序),本质是一堆指令和数据的集合,只要不删除就永久存在。 - 进程(Process) :程序的一次运行实例,是动态的,有完整的生命周期(创建 → 调度运行 → 退出销毁)。它是操作系统分配资源(内存、CPU 时间、文件句柄等)的基本单位。
💡 通俗类比:
程序 = 印在纸上的菜谱(静态不变,文字永远在那)
进程 = 按照菜谱炒菜的整个过程(从开火备料到出锅,动态进行,有开始有结束,占用锅、燃气、食材等实际资源)
2. 进程的核心特点
- 并发性:系统里可以同时存在多个进程,由操作系统按时间片调度切换执行,宏观上看起来 "同时运行"。
- 独立性:每个进程拥有独立的虚拟内存空间,互相隔离;一个进程崩溃不会影响其他进程。
- 动态性:有完整的生命周期和状态变化,从创建到销毁是一个完整过程。
二、PCB 是什么?
1. 定义
PCB 全称 Process Control Block(进程控制块) ,是操作系统内核中专门用来管理和描述进程的核心数据结构。
你可以把它理解为每个进程的「身份证 + 档案袋」:操作系统每创建一个进程,就会生成一个对应的 PCB,里面记满了这个进程的全部信息。操作系统完全靠 PCB 来识别进程、调度进程、管理进程占有的所有资源。
2. PCB 里存了什么?
PCB 存储了进程运行所需的全部描述信息,核心分为四大类:
表格
| 分类 | 核心内容 | 作用 |
|---|---|---|
| 标识信息 | PID(进程 ID,全局唯一编号)、进程名、所属用户 / 组 ID | 相当于进程的身份证号,用来唯一区分不同进程 |
| 上下文现场 | CPU 寄存器的值(程序计数器 PC、栈指针 SP、通用寄存器等) | 这是多任务切换的核心:进程被切走时,把当前 CPU 的状态存进 PCB;下次切回这个进程时,从 PCB 里把值写回寄存器,进程就能从上次停下的地方继续运行,就像没被打断过一样。 |
| 状态信息 | 就绪、运行、阻塞、挂起、终止等 | 操作系统根据进程状态来决定要不要调度它上 CPU 运行 |
| 资源信息 | 虚拟内存地址映射表、打开的文件描述符列表、信号处理配置、进程优先级、运行统计信息 | 记录进程占有的所有系统资源,相当于它的「资产清单」 |
3. 补充:Linux 中的 PCB
Linux 内核里的 PCB 不是叫 PCB 这个名字,它的具体实现是一个非常庞大的结构体 task_struct,里面包含了上百个字段,除了上面说的基础信息,还涵盖了调度策略、内存管理、文件系统、信号处理、时间统计等全部维度,是内核进程调度的核心依据。
4. 为什么必须要有 PCB?
举个最直观的例子:你一边用浏览器刷网页、一边听歌、一边后台跑着自己写的程序。
CPU 核心数有限,它在这些进程之间快速来回切换,每个进程只运行几毫秒就被切走。
切换的时候,程序算到哪一步了、寄存器里存了什么中间值、打开了哪些文件、内存怎么分配的...... 这些信息必须全部存在 PCB 里。不然切出去再切回来,进程就 "失忆" 了,不知道自己跑到哪、有什么资源。
一句话总结:
进程是运行起来的动态程序,PCB 就是操作系统给每个进程建立的管理档案。
进程==PCB+加载到内存的代码和数据,而不仅仅是代码和数据
操作系统对进程的管理 ==操作系统对链表的增删查改 这也是解耦
三、第一个系统调用:获取进程 PID
3.1 getpid 与 getppid
getpid():获取当前进程的进程 ID(PID)getppid():获取当前进程父进程的进程 ID(PPID)
3.2 代码示例
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
int main()
{
while(1)
{
sleep(1);
cout << "我是一个进程!我的pid: " << getpid()
<< ",我的父进程id: " << getppid() << endl;
}
return 0;
}
3.3 编译运行
# 编译
g++ myprocess.cpp -o myprocess
# 运行
./myprocess
程序每秒打印一次自己的 PID 和父进程 PID。
你会发现一个有趣的现象:每次重新运行程序,PID 都不一样,但父进程 PID 始终不变 。
这个不变的父进程是谁?就是你当前的终端 Shell(bash /zsh 等)。命令行解释器本身也是一个进程,你输入的每一条命令,都是由它创建子进程去执行的。
四、查看系统中的进程
4.1 ps 命令
ps axj 是最常用的进程查看命令,可以列出系统中所有进程的详细状态信息:
ps axj
核心字段:
PID:进程 IDPPID:父进程 IDSTAT:进程状态TIME:累计运行时间COMMAND:执行的命令
4.2 高效过滤技巧
进程太多找特定程序时,配合管道和 grep 过滤:
# 只看名字包含 myprocess 的进程
ps axj | grep myprocess
但这样会把 grep 自己这个进程也搜出来,优化写法:
# 排除 grep 自身进程
ps axj | grep myprocess | grep -v grep
如果想保留表头:
# 先输出表头,再输出过滤结果
ps axj | head -1 && ps axj | grep myprocess | grep -v grep
4.3 终止进程
# 强制杀死指定 PID 的进程
kill -9 进程PID
-9 是最强制的杀死信号,进程无法捕获,直接终止。
五、/proc 文件系统:进程的虚拟文件视图
Linux 有一个非常特殊的目录 /proc,它不是真实的磁盘文件,而是内核虚拟出来的:系统里的每个进程,都会在 /proc 下对应一个以 PID 命名的目录,里面用文件的形式展示进程的所有信息。进程结束,对应目录就自动消失。
5.1 两个核心文件
进入某个进程的目录,两个最核心的文件:
ls /proc/进程PID -l
表格
| 文件 | 作用 |
|---|---|
exe |
软链接,指向这个进程对应的可执行文件路径(程序从哪来的) |
cwd |
软链接,指向进程的当前工作目录 |
5.2 进程的工作目录(CWD)
进程运行时,有一个「当前工作目录」的属性。当我们代码里用相对路径打开文件时,系统会自动把「cwd 路径 + 相对路径」拼接起来,定位最终文件位置。
💡 常见疑问:为什么新建的文件默认出现在执行命令的目录?
因为进程启动时,cwd 默认继承自父进程(也就是 Shell)所在的目录。你在哪个目录执行命令,进程的工作目录就在哪。
5.3 动态修改工作目录:chdir
进程可以通过 chdir() 系统调用,动态修改自己的工作目录:
#include <unistd.h>
int main()
{
chdir("/home/whb"); // 将工作目录切换到 /home/whb
FILE* fp = fopen("hello.txt", "a"); // 文件会创建在 /home/whb 下
return 0;
}
你每天用的 Shell 命令
cd,本质就是内部调用了chdir系统调用。
六、进程的父子关系与 fork 系统调用
6.1 进程树:单亲繁殖体系
Linux 的进程是一个单亲繁殖体系,像一棵倒过来的树:
- 最顶端是
systemd / init进程(PID=1),是所有进程的老祖宗 - 每个进程都有唯一的父进程
- 一个进程可以创建多个子进程
我们在 Shell 里执行的所有命令,父进程都是 Shell。
6.2 fork:进程的「分身术」
fork() 是 Linux 系统编程最经典的系统调用,一句话总结:
调用一次,返回两次;一个进程进去,两个进程出来。
它的作用是:以当前进程为模板,复制出一个几乎一模一样的子进程。
- 复制父进程的 PCB、内存数据、文件描述符等
- 父子进程各自独立运行,互不干扰
- 内存空间独立,子进程崩溃不影响父进程
函数原型
#include <unistd.h>
pid_t fork(void);
6.3 最简 fork 代码
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
pid_t _id = fork();
if (_id < 0)
{
// fork 失败
perror("fork fail");
return 1;
}
else if (_id == 0)
{
// 子进程分支
while(1)
{
cout << "我是子进程!我的pid: " << getpid()
<< ",我的父进程id: " << getppid() << endl;
sleep(1);
}
}
else
{
// 父进程分支
while(1)
{
cout << "我是父进程!我的pid: " << getpid()
<< ",子进程id: " << _id << endl;
sleep(1);
}
}
return 0;
}
运行后你会看到:两个 while 循环同时在打印,一个函数里的 if 和 else 居然同时执行了。
七、三个灵魂问题:彻底搞懂 fork
很多初学者第一次看到 fork 都会三观震动:
- 一个函数为什么能有两个返回值?
- if 和 else 为什么可以同时执行?
- 同一个局部变量为什么能同时等于 0 和大于 0?
这三个问题,是理解进程的关键。
7.1 问题一:fork 为什么能返回两个值?
很多人以为函数 return 了就结束了,那 fork 怎么返回两次?
核心答案:fork 函数在 return 之前,就已经把进程分裂成两个了。
fork 内部执行顺序:
- 申请新的 PCB
- 拷贝父进程的 PCB 给子进程,修改 PID、PPID 等信息
- 把子进程加入系统进程调度列表
- 到这一步,已经有两个独立的进程了
然后,父进程和子进程各自执行一次 return:
- 父进程返回:子进程的 PID(大于 0)
- 子进程返回:0
所以不是一个函数返回两次,是两个进程各返回了一次。
7.2 问题二:if 和 else 为什么同时执行?
这是最反直觉的一点。答案也很简单:
代码是共享的,但执行流是独立的。
- 父进程:拿到返回值 > 0,走 if 的父进程分支
- 子进程:拿到返回值 == 0,走 else if 的子进程分支
不是同一个进程同时走了两个分支,是两个进程各走各的分支。两个进程几乎同时打印,视觉上给人一种「代码逻辑被违背」的错觉。
7.3 问题三:同一个变量为什么有两个值?
_id 明明是同一个局部变量,为什么既能等于 0 又能大于 0?
因为进程拥有独立的虚拟内存空间 。fork 之后子进程复制了父进程的内存数据,两个进程的 _id 变量名字一样,但存在于各自独立的内存空间里,是两个完全不同的变量。
- 父进程内存里的
_id= 子进程 PID - 子进程内存里的
_id= 0
二者互不干扰,当然可以同时是不同的值。
八、工程化:Makefile 适配 C++ 代码
之前我们的 Makefile 是 C 语言版本,现在改成 C++ 工程,只需要简单修改几个地方:
SRC=$(wildcard *.cpp)
OBJ=$(SRC:.cpp=.o)
BIN=myprocess
# 编译器换成 g++
CC=g++
$(BIN):$(OBJ)
$(CC) -o $@ $^
%.o:%.cpp
$(CC) -c $<
.PHONY: clean
clean:
rm -f $(OBJ) $(BIN)
核心改动:
- 后缀从
.c换成.cpp - 编译器变量
CC从gcc换成g++
使用方式不变:
make # 编译
make clean # 清理
本篇总结
- 进程是程序的运行实例,是操作系统分配资源的基本单位;PCB 是每个进程的管理档案,存储了进程的全部信息。
- 系统调用 是用户态程序进入内核执行特权操作的接口,
unistd.h是核心入口头文件。 - getpid / getppid 分别获取自身和父进程的 PID;
ps查看系统进程;/proc目录以文件形式展示进程信息。 - fork 是进程分身术:调用一次,分裂成两个独立进程;父进程返回子 PID,子进程返回 0。
- 三个反直觉问题的本质:代码共享、执行流独立、内存空间独立。
下一篇我们将继续深入进程:孤儿进程、僵尸进程、进程等待、进程替换,彻底搞懂进程的完整生命周期。
