OS-HW-3解答
对应题目:exercises/OS-HW-3-2026.pdf
一、选择题
- C
分析:进程的典型内存布局通常包括代码段、数据段、堆和栈等;页表是操作系统用于地址转换和内存管理的数据结构,不属于进程程序本身的典型逻辑组成部分。
- C
分析:程序是存放在磁盘上的静态文件,进程是程序执行时的动态实体。程序本身是被动的,进程才是主动运行、可调度的执行单位。
- C
分析:等待态的进程必须先因为等待事件完成而回到就绪态,然后才可能被调度到运行态,因此“等待态 → 运行态”不能直接发生。
- B
分析:长期调度器决定哪些作业进入系统,从而控制多道程序度;它的执行频率通常远低于频繁进行 CPU 分配的短期调度器。
- C
分析:子进程已经结束,但父进程尚未调用 wait() 或相关系统调用回收其退出状态时,该子进程会暂时保留在进程表中,处于僵尸态。
- B
分析:消息传递不要求通信进程共享同一地址空间,因此更适合分布式系统;共享内存通常更快,但需要显式同步并且通常更适合同一主机内的进程。
- B
分析:当 send() 和 receive() 都阻塞时,发送方和接收方必须同时到达通信点,这种同步通信称为会合(rendezvous)。
- C
分析:fork() 成功后,在子进程中返回值为 0;在父进程中返回值为子进程的 PID;若失败则返回 -1。
- C
分析:PCB 中保存的是进程管理所需的运行信息,例如程序计数器、寄存器、调度信息、内存管理信息和打开文件列表等;源代码文本不存放在 PCB 中。
- C
分析:Chrome 采用多进程架构的关键优势之一是故障隔离。一个标签页对应的渲染进程崩溃时,通常不会直接拖垮其他标签页或整个浏览器。
二、简答题
1. 阅读以下程序,写出所有可能的输出结果,并说明理由。
程序如下:
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid;
int x = 5;
pid = fork();
if (pid == 0) { /* child */
x += 10;
printf("child: x = %d\n", x);
}
else if (pid > 0) { /* parent */
x -= 3;
wait(NULL);
printf("parent: x = %d\n", x);
}
return 0;
}可能输出:
child: x = 15
parent: x = 2理由:
-
fork()之后,父进程和子进程各自拥有变量x的一份独立副本,互不影响。 -
子进程中执行
x += 10,所以子进程中的x变为15,因此输出:
child: x = 15-
父进程中执行
x -= 3,所以父进程中的x变为2。 -
由于父进程调用了
wait(NULL),它会先等待子进程结束,再继续执行printf(),因此父进程一定在子进程输出之后输出:
parent: x = 2- 所以本题在
fork()成功的前提下,只有这一种输出顺序和结果,不会出现父进程先打印的情况。
2. 比较共享内存(Shared Memory)与消息传递(Message Passing)两种进程间通信模型的优缺点。在什么场景下你会倾向于使用哪种模型?
共享内存和消息传递都是常见的 IPC 模型,但它们的设计重点不同。
(1)共享内存(Shared Memory)
优点:
- 速度快:建立共享区后,进程可以直接读写同一块内存,数据交换不必每次都经过内核搬运。
- 适合大数据量通信:例如大量缓冲数据、图像数据、流式数据交换等场景。
- 通信开销低:特别适合同机上的高频通信。
缺点:
- 同步复杂:多个进程同时访问共享区域时,必须额外使用互斥锁、信号量、条件变量等机制。
- 编程更容易出错:如果同步处理不好,会出现竞争条件、数据不一致、死锁等问题。
- 适用范围有限:通常用于同一台机器内、能够共享地址空间映射资源的进程。
(2)消息传递(Message Passing)
优点:
- 抽象清晰:进程通过
send()/receive()交换消息,通信边界明确。 - 更安全、隔离性更好:不直接暴露共享数据区,不容易因为误写共享内存而破坏对方状态。
- 适合分布式系统:通信双方不需要共享内存,适用于网络环境和不同机器上的进程。
缺点:
- 系统开销相对更大:通常需要内核介入、消息复制、缓冲管理等。
- 大数据传输效率可能较低:频繁发送大块数据时性能不如共享内存。
- 接口和缓冲策略设计更复杂:例如阻塞/非阻塞、同步/异步、直接/间接通信等都要考虑。
(3)使用倾向
- 如果是同一台机器上的高性能通信,尤其是大量数据交换、低延迟要求较高时,我更倾向于使用共享内存。
- 如果更强调模块解耦、通信边界清晰、分布式适配能力或安全隔离,我更倾向于使用消息传递。
- 在工程实践中,也常见“共享内存传数据,消息传递做控制”的组合方式。
3. 什么是僵尸进程(Zombie Process)?什么是孤儿进程(Orphan Process)?它们分别是如何产生的,操作系统如何处理它们?
(1)僵尸进程(Zombie Process)
僵尸进程是指:子进程已经执行结束,但父进程还没有调用 wait() / waitpid() 回收其退出状态时,残留在进程表中的进程项。
产生原因:
- 子进程先终止。
- 内核需要暂时保留它的 PID、退出码、统计信息等,供父进程读取。
- 如果父进程迟迟不回收,这个已经结束的子进程就处于僵尸态。
操作系统处理方式:
- 僵尸进程本身已经不再运行,也不再占用 CPU。
- 但它仍占用一个进程表项。
- 当父进程调用
wait()/waitpid()后,内核才会真正删除该进程的残留信息。
(2)孤儿进程(Orphan Process)
孤儿进程是指:父进程先于子进程结束,而子进程仍在继续运行的进程。
产生原因:
- 父进程异常退出或正常结束。
- 子进程还没有结束,因此失去了原来的父进程。
操作系统处理方式:
- 操作系统会把孤儿进程重新交给
init(传统 UNIX 中 PID 1)或等价的收养进程管理。 - 该新父进程会在孤儿进程终止后调用
wait()回收它,避免其长期变成僵尸。
(3)二者区别
- 僵尸进程:子进程已经死了,但进程表项还没被回收。
- 孤儿进程:子进程还活着,只是父进程先没了。
4. 阅读以下程序,分析总共会产生多少个进程(包括初始进程),并解释 “LINE J” 处的 printf 会被执行多少次。
程序如下:
#include <stdio.h>
#include <unistd.h>
int main()
{
int i;
for (i = 0; i < 3; i++)
fork();
printf("hello\n"); /* LINE J */
return 0;
}结论:
- 总共会产生 8 个进程(包括最初的 1 个进程)。
LINE J处的printf("hello\n");会被执行 8 次。
分析过程:
-
初始时只有 1 个进程。
-
第 1 次循环执行
fork()后,1 -> 2个进程。 -
第 2 次循环时,这 2 个进程都会再次执行
fork(),于是变成2 -> 4个进程。 -
第 3 次循环时,这 4 个进程都会再次执行
fork(),于是变成4 -> 8个进程。 -
循环结束后,所有 8 个进程都会继续执行后面的
printf("hello\n");,因此该语句总共执行 8 次。
一般地,若无条件执行 n 次 fork(),则最终总进程数为:
2^n本题中 n = 3,所以总进程数是 2^3 = 8。
三、分析题
1. 在教材中介绍的生产者-消费者问题中,共享缓冲区使用循环数组实现(BUFFER_SIZE = 10),通过 in 和 out 两个指针来管理。教材指出这种方案最多只能使用 BUFFER_SIZE - 1 = 9 个缓冲项。
(a)请解释为什么该方案只能使用 BUFFER_SIZE - 1 个缓冲项,而不是 BUFFER_SIZE 个。
在只使用 in 和 out 两个指针管理循环缓冲区时,会遇到一个关键问题:如果仅观察 in 和 out 的值,那么“缓冲区为空”和“缓冲区已满”都有可能表现为相同的指针关系。
通常定义:
in指向下一个将被写入的位置out指向下一个将被取出的位置
如果不增加额外状态,那么:
- 当
in == out时,通常约定表示空
为了避免“满”和“空”混淆,系统就必须主动保留一个空槽位。于是把“满”定义为:
(in + 1) % BUFFER_SIZE == out这意味着只要再放入一个元素就会让 in == out,从而无法区分“满”还是“空”,所以这个最后的位置不能使用。
因此,当 BUFFER_SIZE = 10 时,最多只能实际存放 9 个元素。
(b)请设计一种改进方案,使得所有 BUFFER_SIZE 个缓冲项都可以被利用。给出关键的数据结构和判空/判满条件。
一种常见改进方式是:除了 in 和 out 之外,再增加一个计数器 count,表示当前缓冲区中已有多少个元素。
这样就能明确区分“空”和“满”,从而把全部 BUFFER_SIZE 个槽位都利用起来。
关键数据结构:
#define BUFFER_SIZE 10
typedef struct {
item buffer[BUFFER_SIZE];
int in;
int out;
int count;
} circular_buffer;初始状态:
in = 0;
out = 0;
count = 0;判空条件:
count == 0判满条件:
count == BUFFER_SIZE生产者放入一个元素:
buffer[in] = new_item;
in = (in + 1) % BUFFER_SIZE;
count++;消费者取出一个元素:
item x = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;这种改进的优点:
- 可以使用全部
BUFFER_SIZE个缓冲项; - “空”和“满”的判断不再冲突;
- 循环队列逻辑仍然保留,结构清晰。
当然,在并发环境下,in、out、count 的访问仍需要配合同步机制,例如互斥锁、信号量等,以保证生产者和消费者并发访问时的数据一致性。
汇总答案
选择题答案
1.C 2.C 3.C 4.B 5.C 6.B 7.B 8.C 9.C 10.C