进程的属性
Linux 进程:程序运行后的完整实体
1. 进程是什么
磁盘上的程序只是代码和数据:
程序
↓ 执行
进程程序是静态的,进程是程序运行后的动态实体。
进程不仅包含程序本身,还包含运行它所需要的各种资源和状态。
可以直接记成:
进程 = 运行中的程序 + 它占有的资源 + 它的运行状态
2. 一个进程到底拥有些什么
Linux 会为每个进程维护一组信息:
进程
├── PID / PPID
├── 虚拟地址空间
│ ├── 代码段
│ ├── 数据段
│ ├── 堆
│ ├── 栈
│ └── 共享库等映射
├── 页表
├── 文件描述符表
├── 信号相关信息
├── UID / GID
└── 调度与运行状态所以,PID 只是进程的身份证,不等于进程本身。
3. PID 和 PPID
每个进程都有自己的 PID:
getpid();用于标识当前进程。
例如:
父进程 PID = 1000
│
fork()
↓
子进程 PID = 1200那么:
子进程:
PID = 1200
PPID = 1000获取:
getpid(); // 当前进程 PID
getppid(); // 父进程 PID4. 进程状态
进程创建后,并不是一直在运行,而是在不同状态之间切换。
常见状态:
R → Running / Runnable
S → 可中断睡眠
D → 不可中断睡眠
T → 停止
Z → 僵尸例如:
sleep(3);进程通常进入:
R
↓
S
↓
3秒后
↓
R所以进程状态描述的是:
这个进程当前能不能运行、为什么没有运行。
5. 虚拟地址空间
每个进程都有自己的虚拟地址空间。
粗略理解:
┌──────────────────┐
│ 栈 │
├──────────────────┤
│ 共享库 / mmap区域 │
├──────────────────┤
│ 堆 │
├──────────────────┤
│ 数据段 / BSS │
├──────────────────┤
│ 代码段 │
└──────────────────┘其中:
代码段
存放程序指令。
数据段 / BSS
存放全局变量、静态变量等。
堆
主要用于动态内存:
malloc();
free();栈
用于函数调用,例如:
void func()
{
int a = 10;
}局部变量 a 通常位于栈上。
6. 为什么每个进程都有自己的地址空间
假设:
进程 A:
int a = 10;
进程 B:
int a = 20;两个进程都可以拥有自己的:
虚拟地址 0x1000但它们最终映射到不同的物理内存。
进程 A 进程 B
│ │
0x1000 0x1000
│ │
↓ ↓
物理页 A 物理页 B这就是:
进程之间的地址空间隔离。
7. 页表是什么
CPU 访问的是虚拟地址,而真正的物理内存位置需要通过页表完成映射:
虚拟地址
↓
页表
↓
物理地址每个进程拥有自己的地址空间映射关系,因此一个普通进程不能直接访问另一个进程的普通内存。
所以:
页表是实现进程虚拟地址空间隔离的关键基础。
8. 文件描述符表
进程除了内存,还需要管理打开的资源。
Linux 用文件描述符统一表示很多对象:
0 → stdin
1 → stdout
2 → stderr
3 → 普通文件
4 → Socket
5 → Pipe例如:
int fd = open("test.txt", ...);得到一个文件描述符。
Socket 也是类似:
int fd = socket(...);因此每个进程都有自己的:
文件描述符表
它记录当前进程打开了哪些文件、Socket、Pipe、设备等。
9. 信号相关信息
进程还需要维护自己的信号状态,例如:
signal(SIGINT, handler);表示:
当前进程收到
SIGINT时,应该怎么处理。
因此一个进程不仅有:
内存
文件还有:
信号处理方式
待处理信号
信号屏蔽状态10. UID 和 GID
Linux 还必须知道:
这个进程是谁?它有什么权限?
所以进程拥有:
UID → 用户身份
GID → 用户组身份系统进行文件、设备等资源的访问控制时,会结合这些身份信息判断权限。
11. 进程为什么需要调度
一个 CPU 核同一时刻只能真正执行一个线程。
Linux 中多个执行流需要竞争 CPU:
进程/线程 A
进程/线程 B
进程/线程 C
↓
调度器
↓
CPU 轮流执行调度器负责决定:
下一时刻谁获得 CPU。
严格来说,在 Linux 中:
线程是 CPU 调度的基本执行单位,而进程主要承担资源和地址空间的隔离与管理。
所以“进程是资源分配和调度的基本单位”是教材里的经典说法,但在现代 Linux 语境下,最好把资源分配和CPU 调度区分开。
12. fork() 为什么会产生两个进程
pid_t pid = fork();执行前:
进程 A执行后:
fork()
/ \
↓ ↓
父进程 子进程父子进程拥有各自的进程身份和地址空间,但初始内容具有继承关系。
因此你前面学到的:
Pipe
共享内存
信号量
Signal
waitpid()大量都建立在 fork() 创建父子进程的基础上。
13. 僵尸进程
子进程结束后:
子进程
↓
exit()
↓
执行结束如果父进程没有:
wait();
waitpid();回收它的退出状态,子进程可能处于:
Z → Zombie僵尸进程不是“还在运行”,而是:
进程已经结束,但父进程还没有完成回收。
14. 进程和线程
可以这样理解:
进程
├── 地址空间
├── 文件
├── 资源
├── 权限
└── 多个线程
├── 线程1
├── 线程2
└── 线程3同一进程中的线程通常共享:
代码
全局数据
堆
打开的文件
地址空间但各自拥有:
栈
寄存器状态
执行状态所以:
进程更像资源容器,线程更像真正执行代码的执行流。
15. 为什么需要 IPC
现在就能理解你前面学的 IPC 了。
因为:
进程 A
┌──────────────┐
│ 独立地址空间 │
└──────────────┘
进程 B
┌──────────────┐
│ 独立地址空间 │
└──────────────┘A 不能直接读取 B 的普通内存。
如果两个进程需要协作,就需要:
Pipe
FIFO
消息队列
共享内存
信号量
Signal
Socket所以:
IPC 的根本原因,就是进程之间默认拥有相互隔离的地址空间。
16. 把整个进程串起来
程序
↓
执行
↓
创建进程
↓
Linux 为它建立运行环境
↓
┌─────────────────────┐
│ PID / PPID │
│ 虚拟地址空间 │
│ 页表 │
│ 文件描述符表 │
│ 信号状态 │
│ UID / GID │
│ 调度状态 │
└─────────────────────┘
↓
进入运行 / 睡眠 / 停止等状态
↓
最终退出所以真正应该记住的不是一堆定义,而是这句话:
进程不是“一个正在运行的程序”这么简单,而是 Linux 为程序提供的一整个运行实体:有自己的地址空间、页表、文件描述符、权限、信号状态和执行状态。
而后面的:
fork() → 创建进程
exec() → 装载新程序
wait() → 回收子进程
IPC → 进程之间通信
Signal → 进程之间通知
Socket → 进程/主机之间通信本质上,都是围绕这个“进程实体”展开的。

