欢迎光临
我们一直在努力

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 都会三观震动:

  • 一个函数为什么能有两个返回值?
  • 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。
  • 三个反直觉问题的本质:代码共享、执行流独立、内存空间独立。
  • 下一篇我们将继续深入进程:孤儿进程、僵尸进程、进程等待、进程替换,彻底搞懂进程的完整生命周期。 在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » Re:Linux 系统编程篇 (十二):进程篇(一)基础与 fork 系统调用全解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址