Linux 进程概念:从硬件到操作系统,再到进程管理
本文为 Linux 系统编程系列的第一篇,从冯·诺依曼体系结构出发,深入理解操作系统的管理思想,引出进程的核心概念(PCB、task_struct),并详细讲解如何查看进程、创建进程(fork),以及父子进程的代码共享、数据写时拷贝等关键知识。帮助同学们建立对进程的完整认知。
一、上节课核心回顾:先描述,再组织
在正式进入进程之前,我们需要先回顾上节课最重要的结论:
管理的本质:先描述,再组织。
-
校长管理学生,不需要和学生见面,只需要通过数据(学籍、成绩等)来管理。
-
数据由辅导员(相当于驱动程序)收集,录入教务系统(相当于操作系统内核的数据结构)。
-
校长的日常管理转化为对数据的增删查改。
-
在计算机中,操作系统管理硬件、进程、内存等,同样遵循这一原则:先用结构体描述对象,再用链表等数据结构组织起来。
为什么 C++ 要有类和 STL?
类解决“描述”,STL 容器解决“组织”。面向对象语言成为主流,不是因为语言设计者的偏好,而是因为世界本身就是先描述再组织的。
二、系统调用与库函数
2.1 银行窗口类比
-
操作系统不相信任何用户,但又必须提供服务。
-
就像银行:不让你进金库,但通过窗口给你办理业务。
-
操作系统提供的“窗口”就是系统调用(System Call)。
2.2 系统调用的本质
-
系统调用是操作系统暴露给上层的C风格函数接口。
-
用户通过系统调用访问操作系统内核的数据和功能(如创建进程、读写文件等)。
-
系统调用使用起来比较底层、复杂,因此被封装成库函数(如
printf、scanf)。
判断一个库函数底层是否调用了系统调用:
如果该函数最终要访问硬件(显示器、磁盘、网卡等),则一定封装了系统调用。
2.3 系统调用与库函数的关系图
用户程序(你的代码)
↓
库函数(printf, scanf...)
↓
系统调用(write, read, fork...)
↓
操作系统内核
↓
驱动程序
↓
硬件
三、进程的基本概念
3.1 什么是进程?
-
课本定义:程序的一个执行实例,正在执行的程序。
-
内核观点:担当分配系统资源(CPU 时间、内存)的实体。
-
本课程定义:进程 = 内核数据结构(PCB)+ 自己的代码和数据
很多人误以为“把程序加载到内存”就是进程,但严格来说,PCB(进程控制块)才是进程的核心。代码和数据只是进程的“血肉”,PCB 才是“灵魂”。
3.2 为什么要有 PCB?
-
操作系统要管理多个加载到内存的程序,就必须先描述再组织。
-
描述:为每个加载的程序创建一个 PCB(Process Control Block) 结构体,里面存放进程的所有属性(PID、状态、优先级、内存指针等)。
-
组织:将所有 PCB 用链表(或更高效的数据结构)连接起来,形成进程列表。
-
操作系统对进程的管理,就转化为对链表的增删查改。
3.3 PCB 在 Linux 中的具体实现:task_struct
-
PCB 是操作系统学科的抽象概念,不同操作系统有不同实现。
-
在 Linux 中,PCB 的具体结构体叫
task_struct,定义在<linux/sched.h>中。 -
task_struct是一个非常大的结构体(上百个成员),包含:
| 成员分类 | 说明 |
|---|---|
| 标识符 | PID、PPID 等,唯一标识进程 |
| 状态 | 运行、睡眠、停止、僵尸等 |
| 优先级 | 决定进程被调度的先后顺序 |
| 程序计数器 | 下一条要执行的指令地址 |
| 内存指针 | 指向代码段、数据段等 |
| 上下文数据 | 寄存器值(用于进程切换) |
| I/O 状态 | 打开的文件列表、I/O 请求等 |
| 记账信息 | 进程使用的 CPU 时间等 |
在 Linux 内核源码中,
task_struct还包含指向mm_struct(内存描述符)的指针,用于管理进程的虚拟地址空间。
3.4 查看进程
方法一:通过 /proc 文件系统
ls /proc/ # 查看所有进程的 PID 目录ls -l /proc/1234/ # 查看 PID 为 1234 的进程信息
-
/proc是一个内存文件系统,里面的数字目录对应每个进程的 PID。 -
目录下的文件(如
exe、cwd、status)动态反映进程的运行状态。
两个重要的符号链接:
-
exe→ 指向进程对应的可执行文件的绝对路径。 -
cwd→ 指向进程的当前工作目录(Current Working Directory)。
补充知识点:如果把当前工作目录删掉,进程还是会继续跑,后续可能还会有其他影响,当你再去查该进程是exe会标红。

方法二:使用 ps 命令
ps aux # 显示所有进程的详细信息ps axj # 显示进程树(包含 PPID)
ps -l # 显示优先级等信息
-
a:显示一个终端的所有进程 -
x:显示没有控制终端的进程 -
u:以用户为中心显示详细信息 -
j:显示进程组、会话、父进程 ID
示例:查看自己的进程并过滤 grep 自身(需要两个终端一个运行一个查)
ps aux | grep myprocess | grep -v grep
3.5 通过系统调用获取进程 ID
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); // 获取当前进程的 PID pid_t getppid(void); // 获取父进程的 PPID

-
返回值类型
pid_t本质上是整数(int)。 -
这两个都是系统调用(查看手册第 2 章)。
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}

运行后父进程的ppid不变,我们去查这个进程你会发现父进程 PPID 是当前 bash 的 PID。

3.6 命令行解释器 bash 本身也是一个进程
-
每次登录,操作系统都会为用户分配一个
bash进程。 -
bash 不断循环:打印提示符(如
[whb@...]$)→ 等待用户输入命令 → 解析命令 → 创建子进程执行命令 → 回收子进程 → 继续循环。 -
我们执行的所有命令(
ls,ps,./myproc等),都是 bash 的子进程。
四、创建进程:fork 系统调用
4.1 fork 的基本用法
#include <unistd.h> pid_t fork(void);
-
fork()用于创建一个子进程。 -
返回值:
-
成功:父进程返回子进程的 PID,子进程返回 0。
-
失败:返回 -1。
-
特点:
-
子进程是父进程的副本(代码共享,数据写时拷贝)。
-
fork之后,父子进程并发执行(谁先谁后不确定)。
4.2 验证父子进程执行不同代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
} else if (id == 0) {
// 子进程
printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
} else {
// 父进程
printf("I am parent, pid: %d, ppid: %d\n", getpid(), getppid());
}
sleep(1);
return 0;
}
输出示例(每次运行 PID 可能不同):
I am parent, pid: 12345, child pid: 12346 I am child, pid: 12346, ppid: 12345

4.3 fork 的“两个返回值”之谜
-
一个函数怎么会返回两次?
因为fork的核心工作(创建子进程 PCB 并放入运行队列)在return语句之前就已经完成。
return本身也是一条语句,父子进程都会执行这条语句,因此各自返回一次。 -
为什么父进程返回子进程 PID,子进程返回 0?
父进程可能有很多子进程,需要通过不同的 PID 区分它们;子进程只需要知道自己是子进程(0 表示成功),父进程的 PID 可以通过getppid()获得,父与子的比例是1:n。
4.4 fork 之后的代码共享与数据写时拷贝
-
代码共享:子进程复制父进程的
task_struct,其中代码段指针指向同一份物理内存(代码只读,共享安全)。 -
数据写时拷贝(Copy-on-Write, COW):
-
默认情况下,父子进程的数据段也指向同一份物理内存。
-
当其中一方尝试修改数据时,操作系统会为该进程单独拷贝一份数据副本,然后修改副本。
-
这样,父子进程的数据就互相独立了,保证了进程独立性。
-
验证实验:
int g_val = 100;
int main()
{
pid_t id = fork();
if (id == 0) {
// child
g_val = 200;
printf("child: g_val = %d, &g_val = %p\n", g_val, &g_val);
} else {
sleep(2);
printf("parent: g_val = %d, &g_val = %p\n", g_val, &g_val);
}
return 0;
}
输出(地址相同但值不同):
child: g_val = 200, &g_val = 0x601038 parent: g_val = 100, &g_val = 0x601038
同一虚拟地址映射到不同的物理地址 → 虚拟地址空间的引入。后续课程会详细讲解。
五、进程的独立性
-
独立性:一个进程挂掉,不会影响其他进程,即使是父子进程也不例外。
-
内核数据结构(PCB)各自独立,代码只读共享不影响,数据通过写时拷贝私有化。
-
因此,操作系统可以同时运行大量进程,互不干扰。
六、本节课总结
| 知识点 | 核心内容 |
|---|---|
| 管理思想 | 先描述(struct),再组织(链表) |
| 系统调用 | 操作系统提供的接口,用户通过它访问内核 |
| 进程定义 | 进程 = PCB(task_struct)+ 代码和数据 |
| 查看进程 | ps aux、/proc 文件系统 |
| 获取 PID | getpid()、getppid() |
| 创建进程 | fork(),父进程返回子进程 PID,子进程返回 0 |
| 代码与数据 | 代码共享,数据写时拷贝(COW) |
| 进程独立性 | 内核数据结构独立 + 写时拷贝保证互不干扰 |
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
ls -l /proc/1234/ # 查看 PID 为 1234 的进程信息
ps axj # 显示进程树(包含 PPID)
ps -l # 显示优先级等信息



所有评论(0)