Re:Linux 系统编程篇 (十二):进程篇(一)基础与 fork 系统调用全解


观众老爷们大家好 这里是邪修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:进程 ID
  • PPID:父进程 ID
  • STAT:进程状态
  • 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 都会三观震动:

  1. 一个函数为什么能有两个返回值?
  2. if 和 else 为什么可以同时执行?
  3. 同一个局部变量为什么能同时等于 0 和大于 0?

这三个问题,是理解进程的关键。

7.1 问题一:fork 为什么能返回两个值?

很多人以为函数 return 了就结束了,那 fork 怎么返回两次?

核心答案:fork 函数在 return 之前,就已经把进程分裂成两个了。

fork 内部执行顺序:

  1. 申请新的 PCB
  2. 拷贝父进程的 PCB 给子进程,修改 PID、PPID 等信息
  3. 把子进程加入系统进程调度列表
  4. 到这一步,已经有两个独立的进程了

然后,父进程和子进程各自执行一次 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)

核心改动:

  1. 后缀从 .c 换成 .cpp
  2. 编译器变量 CCgcc 换成 g++

使用方式不变:

复制代码
make        # 编译
make clean  # 清理

本篇总结

  1. 进程是程序的运行实例,是操作系统分配资源的基本单位;PCB 是每个进程的管理档案,存储了进程的全部信息。
  2. 系统调用 是用户态程序进入内核执行特权操作的接口,unistd.h 是核心入口头文件。
  3. getpid / getppid 分别获取自身和父进程的 PID;ps 查看系统进程;/proc 目录以文件形式展示进程信息。
  4. fork 是进程分身术:调用一次,分裂成两个独立进程;父进程返回子 PID,子进程返回 0。
  5. 三个反直觉问题的本质:代码共享、执行流独立、内存空间独立。

下一篇我们将继续深入进程:孤儿进程、僵尸进程、进程等待、进程替换,彻底搞懂进程的完整生命周期。

相关推荐
牢姐与蒯21 分钟前
Linux进程(五).之环境变量
linux·运维·服务器·ubuntu
ZGi.ai21 分钟前
ZGI 文件产物:生成报告后怎样交付
大数据·运维·workflow·权限管理·zgi·智能体交付·文件产物
楠楠子呀31 分钟前
号码状态核验:从数据清洗到合规处理的实现思路
运维·自动化
raindayinrain37 分钟前
深入理解Linux内核--系统调用,性能优化
linux·性能优化·系统调用·返回值·入参·内核态用户态地址访问
小周学学学41 分钟前
GLPI 项目解读:开源 IT 资产管理与 ITSM 服务台平台
运维·开源·自动化·资产管理
码上有光43 分钟前
Linux:进程控制和进程替换
java·linux·服务器·进程控制·进程替换
weixin199701080161 小时前
《alibaba.idle.isv.order.ship 接入实录:闲鱼订单发货回传的5个隐式约束》(附Python源码)
服务器·前端·python
爱和冰阔落1 小时前
【Linux】多线程打印为什么会乱?pthread 创建、等待、退出、取消与分离全实战
linux·运维·c++·redis
DYWorker0011 小时前
基于RK3568的Linux驱动开发:实战——WDT
linux·驱动开发