《操作系统》第2章 进程管理 详细教程解析
2.1 进程 vs 程序
核心区别
- 程序:静态的代码和数据的集合,存储在磁盘中(如未运行的
QQ.exe
)。 - 进程:程序的一次动态执行实例,占用内存和 CPU 资源(如运行中的两个 QQ 窗口)。
进程的组成
- 代码段(Text Segment)
- 存放可执行指令(如 QQ 的登录验证逻辑)。
- 特点:只读,避免进程间代码冲突。
- 数据段(Data Segment)
- 包含全局变量、静态变量和堆内存(如 QQ 的聊天记录缓存)。
- PCB(Process Control Block)
- 操作系统的“进程身份证”,记录关键信息:
- 进程 ID(PID):唯一标识符(如 Linux 中通过
ps
命令查看 PID)。 - 进程状态:运行、就绪、阻塞等(见 2.2)。
- 程序计数器(PC):指向下一条要执行的指令地址。
- 寄存器值:保存 CPU 上下文(如进程切换时恢复现场)。
- 优先级:影响调度顺序(如实时进程优先级高于普通进程)。
- 进程 ID(PID):唯一标识符(如 Linux 中通过
- 操作系统的“进程身份证”,记录关键信息:
示例
- 双击打开
QQ.exe
→ 操作系统创建进程,分配 PID 为 1234。 - 再次双击
QQ.exe
→ 创建新进程,PID 为 5678,两个进程独立运行。
2.2 进程状态转换
五状态模型
状态 | 描述 | 触发条件 |
---|---|---|
新建 | 进程刚被创建,尚未加载到内存(如双击程序图标)。 | 用户或系统启动新任务。 |
就绪 | 进程已获得除 CPU 外的所有资源,等待调度(如下载任务排队)。 | 新建进程初始化完成或被唤醒。 |
运行 | 进程占用 CPU 执行指令(如解压文件时 CPU 占用率 100%)。 | 调度程序分配时间片。 |
阻塞 | 进程等待外部事件(如用户输入、磁盘 I/O 完成)。 | 发起 I/O 请求或等待信号量。 |
终止 | 进程执行完毕或被强制终止(如任务管理器结束进程)。 | 正常退出或收到终止信号(如 kill )。 |
状态转换图
新建 → 就绪 ↔ 运行 → 阻塞 → 终止
↑______________|
- 转换详解
- 就绪 → 运行:调度程序选择该进程(如时间片轮转)。
- 运行 → 就绪:时间片用完或被更高优先级进程抢占。
- 运行 → 阻塞:进程主动等待资源(如
read()
系统调用等待键盘输入)。 - 阻塞 → 就绪:等待的事件完成(如磁盘数据就绪)。
实例
- 播放视频时:
- 运行状态:解码视频帧占用 CPU。
- 阻塞状态:等待下一帧数据从磁盘加载。
2.3 进程同步
临界资源问题
- 定义:多个进程共享的资源(如打印机、共享变量),需互斥访问。
- 问题:竞态条件(Race Condition)导致数据不一致。
- 示例:两人同时编辑文档,未保存的修改可能被覆盖。
解决方案:信号量(Semaphore)
- 信号量类型:
- 二进制信号量:取值为 0 或 1,用于互斥(如打印机锁)。
- 计数信号量:表示剩余资源数量(如缓冲区空闲槽位数)。
- 操作:
- P(Proberen,等待):
P(semaphore s) { s.value--; if (s.value < 0) { 将进程加入阻塞队列; } }
- V(Verhogen,释放):
V(semaphore s) { s.value++; if (s.value <= 0) { 从阻塞队列唤醒一个进程; } }
- P(Proberen,等待):
经典同步问题
-
生产者-消费者问题
- 场景:生产者向缓冲区放数据,消费者取数据。
- 同步条件:
- 缓冲区满时生产者等待(
empty
信号量)。 - 缓冲区空时消费者等待(
full
信号量)。 - 互斥访问缓冲区(
mutex
信号量)。
- 缓冲区满时生产者等待(
- 伪代码:
// 初始化 semaphore empty = N; // 缓冲区初始为空槽数 semaphore full = 0; // 初始无数据 semaphore mutex = 1; // 互斥锁 // 生产者 producer() { while (1) { 生产数据; P(empty); // 申请空槽 P(mutex); // 申请缓冲区访问权 放入缓冲区; V(mutex); V(full); // 增加数据计数 } } // 消费者 consumer() { while (1) { P(full); P(mutex); 取出数据; V(mutex); V(empty); 消费数据; } }
-
哲学家进餐问题
- 死锁场景:所有哲学家同时拿起左侧叉子,导致无限等待。
- 解决方案:
- 限制最多 4 人同时进餐(破坏“循环等待”条件)。
- 奇数号哲学家先拿左叉子,偶数号先拿右叉子(破坏对称性)。
2.4 进程通信(IPC)
三种主要方式
-
共享内存(Shared Memory)
- 机制:多个进程直接访问同一块内存区域。
- 优点:速度快(无需内核介入)。
- 缺点:需自行处理同步(如用信号量)。
- 示例:
- 剪贴板:进程 A 复制数据到共享内存,进程 B 从中读取。
- 代码片段(Linux):
// 创建共享内存 int shm_id = shmget(IPC_PRIVATE, size, 0666|IPC_CREAT); char *shm_ptr = shmat(shm_id, NULL, 0); // 写入数据 strcpy(shm_ptr, "Hello, shared memory!"); // 分离共享内存 shmdt(shm_ptr);
-
消息传递(Message Passing)
- 机制:进程通过发送/接收消息交互,由内核管理。
- 类型:
- 直接通信:发送方指定接收方(如进程 A → 进程 B)。
- 间接通信:通过“邮箱”(消息队列)传递。
- 示例:
- 微信发送消息:发送进程将消息写入内核队列,接收进程读取。
- 代码片段(Linux 消息队列):
// 发送消息 struct msgbuf { long mtype; char mtext[100]; }; msgsnd(msgid, &msg, sizeof(msg.mtext), 0); // 接收消息 msgrcv(msgid, &msg, sizeof(msg.mtext), mtype, 0);
-
管道(Pipe)
- 机制:单向字节流,基于文件描述符(FIFO)。
- 限制:
- 匿名管道仅用于父子进程间通信。
- 命名管道(FIFO)可用于任意进程。
- 示例:
- 命令行
ls | grep "txt"
:ls
的输出通过管道传给grep
。 - 代码片段(匿名管道):
int fd[2]; pipe(fd); // 创建管道 if (fork() == 0) { close(fd[0]); write(fd[1], "Hello pipe!", 12); } else { close(fd[1]); read(fd[0], buffer, 12); }
- 命令行
关键对比表
通信方式 | 速度 | 同步需求 | 适用场景 |
---|---|---|---|
共享内存 | 最快 | 需额外同步机制 | 高频数据交换(如视频渲染) |
消息传递 | 中等 | 内核自动同步 | 跨机器通信(如网络消息) |
管道 | 较慢 | 读写阻塞保证同步 | 简单父子进程通信 |
思考题
-
为什么进程切换需要保存 PCB 中的寄存器值?
- 答:恢复进程运行时需还原 CPU 上下文(如程序计数器、堆栈指针),确保指令连续执行。
-
若信号量初始值为 5,10 个进程同时执行 P 操作,最终有多少进程被阻塞?
- 答:5 个进程成功,剩余 5 个进程使信号量减为 -5,全部阻塞。
-
管道为什么是单向的?如何实现双向通信?
- 答:单向设计简化同步;双向通信需创建两个管道(一个读,一个写)。
本章深入解析进程管理的核心机制,理解这些概念是掌握多任务调度、资源竞争解决和高效通信的基础。