【专业课学习】《操作系统》期末考试模拟试卷(仅供应试!!!)

66 阅读19分钟

8e11c59bc4549657e631d89acc178cfa.jpg

一、选择题

(1) 系统调用是( )。

A、用户编写的一个子程序

B、高级语言中的标准库函数

C、操作系统中的一条命令

D、操作系统向用户程序提供的接口

答案: D

(2) 配置了操作系统的计算机是一台比原来的物理计算机功能更强的计算机,这样的计算机可以看作是一台逻辑上的计算机,称为( )计算机。

A、并行

B、真实

C、虚拟

D、共享

答案: C

(3) 程序中发出的一个陷阱指令,通常的作用是( )。

A、表示一个I/O操作的完成

B、调用其他用户进程的一段程序

C、捕获了硬件错误

D、调用操作系统功能

答案: D

(4) 如果当前系统中的作业都是同时到达的,那么使作业平均周转时间最短的作业调度算法是( )。

A、FCFS

B、最短作业优先

C、时间片轮转

D、前台时间片轮转、后台FCFS调度

答案: B

解析:

周转时间=任务结束时间-任务到达时间=任务等待时间+任务执行时间

现在题目要求我们考虑N个任务周转时间的平均情况,即平均周转时间=(任务1的周转时间 + 任务2的周转时间 + ... + 任务N的周转时间) / N

进一步变换公式,可得平均周转时间=(任务1的等待时间 + 任务2的等待时间 + ... + 任务N的等待时间) / N + (任务1的执行时间 + 任务2的执行时间 + ... + 任务N的等待时间) / N

由于待处理的任务既定,因此公式中(任务1的执行时间 + 任务2的执行时间 + ... + 任务N的等待时间) / N这部分内容无法改变。 因此,我们的目标应该是让公式(任务1的等待时间 + 任务2的等待时间 + ... + 任务N的等待时间)这部分内容的取值尽可能小。

A. FCFS采用先来先到的策略,因此若短任务排在后面,则会使其任务等待时间明显上升(超过了长任务等待短任务执行结束的时间成本),从而使得平均任务周转时间上升,排除。

B. SJF采用短任务优先的策略,因此不存在FCFS中短任务排在后面而导致其等待时间过长的问题。同时对于长任务来说,其需要等待短任务执行完毕,而这部分成本不会很大,不会导致等待时间额外显著增加。因此答案选B。

C. RR算法主要是为了保证公平性和避免饥饿(保证较快的响应时间),并不能保证等待时间最优。该策略要求CPU反复切换执行不同的任务,会导致无法在一个时间片内完成的所有任务都出现额外的等待时间(即它们的结束时间会被整体推后),排除。

D. 这是一种混合调度策略,用于兼顾不同优先级的任务,例如要求即时响应的交互式用户界面(前台)和需要长时间在操作系统中默默运行,而不要求快速响应用户交互的科学计算任务(后台)。题目中并未区分任务类型,且所有任务同时到达,这种策略不适用于此特定场景,或者说其效果不会优于针对单一目标(最小化平均周转时间)设计的SJF。排除。

(5) 绝对路径名访问文件从()开始。

A、当前工作目录

B、用户主目录

C、根目录

D、父目录

答案: C

(6) 假设就绪队列中有10个进程,系统将时间片设为190ms,CPU进行进程切换要花费10ms。则系统开销所占的比率约为 ( ) 。

A、2.5%

B、5%

C、7.5%

D、10%

答案: B

解析:系统开销比率与进程数无关,10ms/(10ms+190ms)=5%

(7) 系统中有12个同类的临界资源,由4个进程共享。资源分配状态如下表:

进程已分配资源最大需求量
P114
P237
P328
P438

为避免死锁,还剩下的3个资源应分配给进程()。

A、P1

B、P2

C、P3

D、P4

答案: P1

(8) 共享存贮器实现临界设备共享的技术是()。

A、虚拟机

B、通道

C、SPOOLing

D、控制器

答案: C

(9) 在页面置换时, ( ) 转换算法有随着分给的页架数的增加而缺页频率也增加的异常现象。

A、FIFO

B、LRU

C、CLOCK(NUR算法)

D、OPT

答案:A

解析:FIFO算法完全不考虑使用频率,即使增加了可供分配的物理页数,使用这些物理页多存储的那部分数据,接下来常访问可能性也不一定大(看运气),也就并不一定有助于增加命中率。

二、系统分析与设计

(1) 考虑下面这段C语言的程序,运行时共会创建几个进程,给出分析过程。

void main() {  
    fork();  
    fork();  
    exit();  
}

【解析】

进程树如下图所示,根进程P1共创建了3个新的子进程。

image.png

(2) 某系统把任一程序分成代码和数据两部分。

CPU知道什么时候要指令(如取指令周期),什么时候要读写数据(如取数据周期或存数据周期)。所以,需要有两组存储保护的寄存器(即两对基地址寄存器和界限寄存器),一组用于指令,另一组用于数据。用于指令的一组寄存器是只读的,以便于多个程序共享。

现在请你分析这种策略的优缺点。

【解析】

题目描述了一种基于分段机制的系统内存管理策略。

优点:

  • 只需两组专用寄存器,实现简单。
  • 针对代码部分的基地址寄存器和界限寄存器是只读的,实现了对代码的保护。
  • 针对代码部分的基地址寄存器和界限寄存器支持多任务共享,有利于节省内存空间。

缺点:

  • 当某个进程需要扩展地址空间时,可能会出现前后内存空间不足的情况,灵活性较差。
  • 随着系统运行过程中不断有进程被创建和销毁,系统内存空间中会逐渐产生大量的内存碎片。

(3) 某款CPU采用一种基于Key的硬件机制来保护内存。

该机制具备如下特点:

  • 内存被划分为固定大小的 64KB 块。
  • 每个 64KB 内存块关联一个16位的“存贮键”。
  • 不同内存块可以关联相同的“存贮键”。
  • 当前在 CPU 上执行的进程也拥有一个16位的“钥匙”。当该进程被调度执行时,操作系统会执行某个特权指令,将其"钥匙"写入CPU的一个专用寄存器mem_key_reg当中,以便于CPU在访存时对当前进程的权限进行检查。
  • 一个进程想要访问某个内存块,CPU会进行进行权限检查。只有满足如下条件之一,访存才能成功:
    • 进程的“钥匙”与目标内存块的“存贮键”匹配
    • 进程的“钥匙”为 0
    • 目标内存块的“存贮键”为 0

针对如下几种不同的计算机系统,请设计合理的内存管理方案,以最有效地利用(或最好地配合)这种基于 64KB 块的硬件保护特性?

  1. 裸机
  2. 单用户系统
  3. 进程总数固定的多用户系统
  4. 进程总数可变的多用户系统
  5. 页式存储管理
  6. 段式存储管理

【解析】

  1. 裸机:
    • 由应用程序自行生成和管理Key,并将自己的代码和数据资源按64KB进行划分及对齐后存入内存。
  2. 单用户系统:
    • 操作系统需为内核进程和用户进程分别一把key_kernelkey_user
    • 当进入内核态时,操作系统将mem_key_reg置为0,以确保内核态程序可以访问任何内存区域。注意,这里我们不能将mem_key_reg置为key_kernel!!!
    • 当进入用户态时,操作系统将用户进程的key刷入mem_key_reg
    • 在为内核或用户进程分配内存时,应按64KB为单位进行分配,以及按64KB进行内存地址对齐。同时,若申请内存者为内核进程,将新分配出来的内存块与key_kernel关联,反之则与key_user关联。
  3. 进程总数固定的多用户系统
    • 需为每个用户进程分配一把Key,其余做法与单用户系统相同。
  4. 进程总数可变的多用户系统
    • 此类系统在底层一般依赖页式存储管理或段式存储管理,需分类讨论。
  5. 页式存储管理
    • 在分页存储管理机制中,系统内存管理和保护的最小单位为页。因此我们只需简单地一个页的大小定义为64KB即可。
  6. 段式存储管理
    • 在分页存储管理机制中,系统内存管理和保护的最小单位为段。因此我们只需简单地规定操作系统分配出去的段的大小必须为64KB的倍数,地址需按64KB对齐,并要求为组成同一个段的内存块关联相同的Key即可。

(4) 某系统采用如下算法来解决两个线程的临界区问题。

bool flag[2] = {false, false};  
int turn;

void Thread0() {
    do {  
        flag[0] = true;  
        turn = 1;  
        while (flag[1] && turn == 1);
        // 临界区代码...
        flag[0] = false;  
        // 剩余代码...  
    } while(1); 
}

void Thread1() {
    do {  
        flag[1] = true;  
        turn = 0;  
        while (flag[0] && turn == 0);
        // 临界区代码...
        flag[1] = false;  
        // 剩余代码...  
    } while(1); 
}

请你分析这套算法能否解决两个线程之间的互斥问题,并说明理由。

【解析】

结论

这个算法是著名的Peterson算法,可以解决两个线程之间的互斥问题!

分析

我们首先证明不可能有两个线程同时处于临界区内。我们用反证法:假设Thread0Thread1都同时处于临界区内。

推理过程如下:

  1. 两个线程进入临界区之前,它们都会先设置自己的flagtrue。即flag[0] = trueflag[1] = true
  2. 它们都会设置turn的值。Thread0设置turn = 1Thread1设置turn = 0。由于这两个赋值操作不是原子的turn的最终值取决于哪个线程最后完成赋值操作。
    • 情况 A: 如果线程0最后设置turn (即turn == 1),那么:
      • Thread0的等待条件是while (flag[1] && turn == 1)。因为flag[1] == trueturn == 1,所以等待条件成立,Thread0阻塞
      • Thread1的等待条件是while (flag[0] && turn == 0)。因为flag[0] == trueturn == 1,所以等待条件不成立,Thread1进入临界区
    • 情况 B: 如果线程1最后设置turn (即turn == 0),那么:
      • 同理可知,Thread1阻塞,而Thread0会进入临界区
  3. 可见在这两种情况下(A和B),只有一个线程能够进入临界区,另一个线程会等待,因此假设不成立,两个线程不可能同时进入临界区。

又注意到,当只有单个线程想要进入临界区时,等待条件显然不成立,即在单个线程的情况下,它一定是可以成功进入临界区的。

因此,Peterson算法的确可以解决两个线程间互斥的问题。

(5) 有一个文件描述符fd,其指向文件的内容字节序列:12, 3, 4, 4, 4, 15, 29, 42, 4, 4, 4, 4, 12, 33, 123, 36, 88。

现在我们假设存在如下系统调用:

  • lseek(int fd, int pos, int n): 定位到文件fd中相对于pos位置的第n个字节。pos的取值可能为:
    • SEEK_SET: 表示文件头
    • SEEK_CUR: 表示当前位置
    • SEEK_END: 表示文件尾
  • read(int fd, char* buf, int n): 从文件fd的当前位置起,读取n个字节到缓冲区buf中去。

请问当如下代码执行完毕后,缓冲区buffer中的内容是什么?

char buffer[10] = { 0 };  // buffer中10个字节全部初始化为0

lseek(fd, 4, SEEK_SET);
read(fd, buffer, 4);

lseek(fd, 3, SEEK_CUR);
read(fd, buffer + 5, 4);

lseek(fd, -1, SEEK_END);
read(fd, buffer + 4, 1);

【解析】

  1. 第一次 read 操作
    • lseek(fd, 4, SEEK_SET):将文件指针定位到位置 4(对应字节值 4)。
    • read(fd, buffer, 4):从位置 4 读取 4 个字节到 buffer[0-3],内容为 4, 15, 29, 42
  2. 第二次 read 操作
    • lseek(fd, 3, SEEK_CUR):文件指针从当前位置 8(第一次读取后自动移动)再移动 3 字节,到达位置 11(对应字节值 4)。
    • read(fd, buffer + 5, 4):从位置 11 读取 4 个字节到 buffer[5-8],内容为 4, 12, 33, 123
  3. 第三次 read 操作
    • lseek(fd, -1, SEEK_END):将文件指针定位到倒数第一个字节(位置 16,对应字节值 88)。
    • read(fd, buffer + 4, 1):将 88 写入 buffer[4]

最终 buffer 的内容为:
[4, 15, 29, 42, 88, 4, 12, 33, 123, 0]

三、算法综合分析

(1) 考虑下面一组进程,进程优先数越小,表示进程的优先级别越高。

进程到达时刻CPU区间优先数
P1043
P2123
P3211
P4434
P5532

请画出在采用下述各调度算法时,系统运行情况的甘特图,并分别计算出采用各调度算法时进程的平均周转时间。假设忽略进程调度及上下文切换开销时间。

  1. FCFS
  2. RR(时间片为1;在同一时刻,刚失去CPU的进程先于新到达的进程进入就绪队列)
  3. 抢占式优先级调度
  4. SJF

【解析】

FCFS

image.png

平均周转时间 = ((4 - 0) + (6 - 1) + (7 - 2) + (10 - 4) + (13 - 5)) / 5 = 5.6

RR

image.png

平均周转时间 = ((7 - 0) + (6 - 1) + (5 - 2) + (12 - 4) + (13 - 5)) / 5 = 6.2

抢占式优先级调度

image.png

平均周转时间 = ((10 - 0) + (5 - 1) + (3 - 2) + (13 - 4) + (8 - 5)) / 5 = 5.4

SJF

image.png

平均周转时间 = ((4 - 0) + (5 - 2) + (7 - 1) + (10 - 4) + (13 - 5)) / 5 = 5.4

(2) 以下是一个计算机系统在某时刻的资源分配情况快照(snapshot)。

image.png

  1. 请用死锁检测算法,分析此时是否存在死锁?
  2. 请介绍应该在何时调用死锁检测算法,以检测死锁。

【解析】

分析是否存在死锁

由于现在系统已没有多余的可分配资源,因此首先只能等待P0执行完毕。

P0执行完毕后,系统可用资源A=0, B=1, C=0。注意到此时系统无法满足任何剩余进程的资源请求,因此系统发生了死锁!

在何时调用死锁检测算法

死锁检测算法并不需要持续运行,因为它本身会消耗系统资源。调用时机通常基于以下策略:

  • 定时检测:  
    • 策略: 每隔一定时间(例如每小时或每几分钟)运行一次检测算法。
    • 优点: 实现简单。
    • 缺点: 可能无法及时发现死锁,且检测间隔设置不当可能导致不必要的开销或检测延迟。
  • 基于阈值的检测:  
    • 策略: 当检测到系统性能指标(如CPU利用率、系统吞吐量)下降到某个预设阈值时,触发死锁检测。
    • 缺点: 这种方法认为性能下降可能是死锁的征兆,但死锁并非性能下降的唯一原因,且死锁不一定会导致系统性能下降(例如在Linux系统中,出现死锁的进程会集体进入睡眠状态)。
  • 请求失败时检测:  当一个进程的资源请求等待一段时间仍无法被满足后,调用死锁检测算法。
    • 优点: 这种方法更具针对性,可能更早发现死锁。
    • 缺点: 如果资源请求失败频繁发生,也会导致检测算法频繁运行,增加系统开销。

(3) 现在有一个页式存储管理系统。

假设某进程对页面的引用序列。具有重复访问连续的一大串页面,再偶发一个随机的页面的特征。如0, 1, ……, 511, 321, 0, 1, ……, 511, 475, 0, 1, ……。即访问序列 0, 1, ……, 511,然后随机引用第 321 页和第 475 页,等等。

请问:

  1. 若分配给该进程的页架数小于访问的最大连续页面序列的长度(如上例中的 512),则标准页面置换算法(LRU、FIFO、Clock)在处理此工作负载时效果较差,这是为什么?
  2. 如果该程序分配了 500 个页架,请设计一种比 LRU、FIFO 或时钟算法性能更好的页面置换算法。

解析

效果较差原因分析

LRU、FIFO、Clock这三种算法都会尝试保留最近刚刚被访问过的页面。这意味着在每一轮循环中,在访问引用序列尾部的那些页面时,循环中最早被访问的那些页面可能会因为系统物理页架耗尽,而被置换出去。但事实上,当一轮循环结束后,这些被置换出去的页面又会被立即访问,因此就会再次从硬盘被调回内存(即颠簸现象),导致处理效率低下。

页面置换算法设计

以下是一种可能的算法方案。

我们可以固定使用第0~498号页架来存储第0~498号页面的数据,这些页架不会参与页面置换。仅保留第499号页架参与页面置换。这种做法可以最大程度抑制"颠簸"现象。

(4) 有一个磁盘有1000个柱面,编号0到999。

磁头上次响应了 200 柱面的请求,现在正在响应 234 柱面上的请求。

现处理按以下顺序到达的一组访问申请:230、345、100、747、245、210、493、875、35、440。

设寻道时移动一个柱面需要 6ms。请分别用下面的磁盘调度算法,计算磁头为响应这个请求序列所需的总时间:

  1. FCFS
  2. SSTF
  3. SCAN
  4. C-SCAN(回扫时间不计入内)
  5. LOOK

【解析】

T(FCFS) = ((234-230)+(345-230)+(345-100)+(747-100)+(747-245)+(245-210)+(493-210)+(875-493)+(875-35)+(440-35))*6ms = 20748ms

T(SSTF) = ((234-230)+(245-230)+(245-210)+(210-100)+(100-35)+(345-35)+(440-345)+(493-440)+(747-493)+(875-747)) * 6ms = 6416ms

T(SCAN)=((245-234)+(345-245)+(440-345)+(493-440)+(747-493)+(875-747)+(999-875)+(999-230)+(230-210)+(210-100)+(100-35)) * 6ms = 10374ms

T(C-SCAN)=((245-234)+(345-245)+(440-345)+(493-440)+(747-493)+(875-747)+(999-875)+(35-0)+(100-35)+(210-100)+(230-210)) * 6ms = 5970ms

T(LOOK) = ((245-234)+(345-245)+(440-345)+(493-440)+(747-493)+(875-747)+(875-230)+(230-210)+(210-100)+(100-35)) * 6ms = 8886ms

注意SCAN算法和LOOK算法的区别。

前者规定磁头移动至磁盘的两端(0号柱面或999号柱面)才改变方向。而后者规定磁头在一个方向上移动至最远的请求,就改变方向。

(5) 已知一个求值公式 (A²+3B) /(B+5A)

若 A、B 已赋值,并在可并发进行运算操作的系统上运行,试以最大并发度为目标,设计出上述计算过程的同步执行方案。

【解析】

假设我们的CPU可用核心数大于等于4,那么一种可能的并发执行方案如下图所示。

该方案通过三阶段流水线化并行计算,在4核CPU上理论加速比可达3倍(第一阶段3线程并行)

image.png

以下是该方案的C语言实现代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 全局变量存储中间计算结果
double A = 2.0;   // 示例输入A
double B = 3.0;   // 示例输入B
double a_square;  // A²结果
double three_B;   // 3B结果
double five_A;    // 5A结果
double numerator; // 分子结果
double denominator;// 分母结果

// 线程函数:计算A²
void* task1(void* arg) {
    a_square = A * A;
    return NULL;
}

// 线程函数:计算3B
void* task2(void* arg) {
    three_B = 3 * B;
    return NULL;
}

// 线程函数:计算5A
void* task3(void* arg) {
    five_A = 5 * A;
    return NULL;
}

// 线程函数:计算分子(A² + 3B)
void* task4(void* arg) {
    numerator = a_square + three_B;
    return NULL;
}

// 线程函数:计算分母(B + 5A)
void* task5(void* arg) {
    denominator = B + five_A;
    return NULL;
}

int main() {
    pthread_t threads[5]; // 存储5个线程ID

    /**************** 第一阶段:并行计算三个乘法 ****************/
    pthread_create(&threads[0], NULL, task1, NULL);
    pthread_create(&threads[1], NULL, task2, NULL);
    pthread_create(&threads[2], NULL, task3, NULL);

    // 等待第一阶段完成
    for (int i = 0; i < 3; ++i) {
        pthread_join(threads[i], NULL);
    }

    /**************** 第二阶段:并行计算分子分母 ****************/
    pthread_create(&threads[3], NULL, task4, NULL);
    pthread_create(&threads[4], NULL, task5, NULL);

    // 等待第二阶段完成
    for (int i = 3; i < 5; ++i) {
        pthread_join(threads[i], NULL);
    }

    /**************** 第三阶段:执行最终除法 ****************/
    if (denominator == 0) {
        fprintf(stderr, "Error: Division by zero!\n");
        exit(EXIT_FAILURE);
    }
    double result = numerator / denominator;

    printf("最终结果:%.2f\n", result);
    return 0;
}