OS-HW-3解答

对应题目:exercises/OS-HW-3-2026.pdf

一、选择题

  1. C

分析:进程的典型内存布局通常包括代码段、数据段、堆和栈等;页表是操作系统用于地址转换和内存管理的数据结构,不属于进程程序本身的典型逻辑组成部分。

  1. C

分析:程序是存放在磁盘上的静态文件,进程是程序执行时的动态实体。程序本身是被动的,进程才是主动运行、可调度的执行单位。

  1. C

分析:等待态的进程必须先因为等待事件完成而回到就绪态,然后才可能被调度到运行态,因此“等待态 → 运行态”不能直接发生。

  1. B

分析:长期调度器决定哪些作业进入系统,从而控制多道程序度;它的执行频率通常远低于频繁进行 CPU 分配的短期调度器。

  1. C

分析:子进程已经结束,但父进程尚未调用 wait() 或相关系统调用回收其退出状态时,该子进程会暂时保留在进程表中,处于僵尸态。

  1. B

分析:消息传递不要求通信进程共享同一地址空间,因此更适合分布式系统;共享内存通常更快,但需要显式同步并且通常更适合同一主机内的进程。

  1. B

分析:当 send()receive() 都阻塞时,发送方和接收方必须同时到达通信点,这种同步通信称为会合(rendezvous)。

  1. C

分析:fork() 成功后,在子进程中返回值为 0;在父进程中返回值为子进程的 PID;若失败则返回 -1

  1. C

分析:PCB 中保存的是进程管理所需的运行信息,例如程序计数器、寄存器、调度信息、内存管理信息和打开文件列表等;源代码文本不存放在 PCB 中。

  1. 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

理由:

  1. fork() 之后,父进程和子进程各自拥有变量 x 的一份独立副本,互不影响。

  2. 子进程中执行 x += 10,所以子进程中的 x 变为 15,因此输出:

child: x = 15
  1. 父进程中执行 x -= 3,所以父进程中的 x 变为 2

  2. 由于父进程调用了 wait(NULL),它会先等待子进程结束,再继续执行 printf(),因此父进程一定在子进程输出之后输出:

parent: x = 2
  1. 所以本题在 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 个进程。

  2. 第 1 次循环执行 fork() 后,1 -> 2 个进程。

  3. 第 2 次循环时,这 2 个进程都会再次执行 fork(),于是变成 2 -> 4 个进程。

  4. 第 3 次循环时,这 4 个进程都会再次执行 fork(),于是变成 4 -> 8 个进程。

  5. 循环结束后,所有 8 个进程都会继续执行后面的 printf("hello\n");,因此该语句总共执行 8 次。

一般地,若无条件执行 nfork(),则最终总进程数为:

2^n

本题中 n = 3,所以总进程数是 2^3 = 8

三、分析题

1. 在教材中介绍的生产者-消费者问题中,共享缓冲区使用循环数组实现(BUFFER_SIZE = 10),通过 in 和 out 两个指针来管理。教材指出这种方案最多只能使用 BUFFER_SIZE - 1 = 9 个缓冲项。

(a)请解释为什么该方案只能使用 BUFFER_SIZE - 1 个缓冲项,而不是 BUFFER_SIZE 个。

在只使用 inout 两个指针管理循环缓冲区时,会遇到一个关键问题:如果仅观察 inout 的值,那么“缓冲区为空”和“缓冲区已满”都有可能表现为相同的指针关系。

通常定义:

  • in 指向下一个将被写入的位置
  • out 指向下一个将被取出的位置

如果不增加额外状态,那么:

  • in == out 时,通常约定表示

为了避免“满”和“空”混淆,系统就必须主动保留一个空槽位。于是把“满”定义为:

(in + 1) % BUFFER_SIZE == out

这意味着只要再放入一个元素就会让 in == out,从而无法区分“满”还是“空”,所以这个最后的位置不能使用。

因此,当 BUFFER_SIZE = 10 时,最多只能实际存放 9 个元素。

(b)请设计一种改进方案,使得所有 BUFFER_SIZE 个缓冲项都可以被利用。给出关键的数据结构和判空/判满条件。

一种常见改进方式是:除了 inout 之外,再增加一个计数器 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 个缓冲项;
  • “空”和“满”的判断不再冲突;
  • 循环队列逻辑仍然保留,结构清晰。

当然,在并发环境下,inoutcount 的访问仍需要配合同步机制,例如互斥锁、信号量等,以保证生产者和消费者并发访问时的数据一致性。

汇总答案

选择题答案

1.C 2.C 3.C 4.B 5.C 6.B 7.B 8.C 9.C 10.C