操作系统核心边界解析:线程、分页、系统调用与并发 简介西安电子科技大学《操作系统》期末试题及答案解析面向准备操作系统课程考试的高校学生、考研复试复习者及需要检验知识掌握程度的自学者帮助梳理闭卷考试中的选择题、填空题等常见题型与核心考点。资源包共1个PDF文件大小约118KB试卷标注考试时间120分钟、四大题、满分100分选择题与填空题各20分题干为中英混合表述内容覆盖进程与线程、CPU调度、分页与虚拟内存、局部性原理、i-node、磁盘访问时间、Linux权限与文件元数据、死锁、信号量及DMA等主题并配有答案解析便于对照题目定位概念盲区、理解出题方式与答题思路。现有207人学习适合在考前进行限时模拟、按章节查漏补缺也可作为课堂复习后的自测材料快速建立操作系统课程的知识框架与题型意识。1. 边界比定义更重要操作系统这门课最反直觉的一点是几乎所有考点都不在定义上而在边界上。进程和线程的边界、用户态和内核态的边界、虚拟内存里虚拟地址和物理地址的边界、调度器在哪些时刻才真正插手——只要边界画错选择题看着每项都像对的大题写满了也未必拿分。这份某高校《操作系统》期末试题资料的价值不在答案字母而在后面那几行解释为什么线程既能实现在用户空间也能实现在内核、为什么 31 位地址拆出来正好是 1K 页、为什么 DMA 能让 CPU 完全袖手旁观。把这些线串成一张从硬件到系统调用的地图「操作系统原理」就不再是需要背的东西。2. 进程与线程内核视角下到底共享了什么2.1 用户级线程与内核级线程差在一次陷入题目里那句「Thread can be implemented in ___」问的其实是线程表由谁维护。答案之所以是「用户空间或内核都可以」是因为线程这个概念本身不绑定实现位置绑定的是「谁来做调度决策」。放到用户空间实现运行时库自己维护一张线程表切换线程就是保存几个 callee-saved 寄存器、换一下栈指针连特权指令都不需要执行/* 用户级线程的上下文切换全程在用户态完成 */ typedef struct uctx { void *sp; /* 保存的栈指针指向该线程自己的栈 */ void (*entry)(void *); void *arg; } uctx_t; extern void ctx_switch(void **old_sp, void *new_sp); /* 汇编实现压 callee-saved 寄存器后换栈 */ void thread_yield(uctx_t *from, uctx_t *to) { ctx_switch(from-sp, to-sp); /* 没有 syscall没有 CR3 切换没有内核栈 */ }这段代码里最关键的信息是「没有」什么没有 CR3 写入页表基址不变没有内核栈切换没有 trapframe 保存。所以用户级线程切换便宜到几十纳秒同一进程内的线程切换几乎免费。代价同样明确内核根本不知道这些线程存在它眼里的这个进程是单线程的。于是任何一个线程执行阻塞式read内核只会把整个进程挂起同进程里其他就绪线程全被连坐。多核也救不了——内核只看到一个可调度实体同一进程的用户级线程永远只能挤在一个核上。反过来在内核里实现线程每个线程在调度器里有一条记录、一个内核栈和一个 TCB创建要走系统调用、切换要陷入内核但阻塞互不影响、能真正并行。混合模型则是把 n 个用户级线程复用到 m 个内核级线程上各取一半好处。2.2 一句话回答「线程不共享什么」「进程内所有线程共享哪些资源」这道题背答案很容易但真正记住要看数据结构长什么样。常见的实现思路是把「属于地址空间的东西」放进进程结构把「属于执行流的东西」放进线程结构struct thread { struct trapframe *tf; /* 私有rax..r15、rip、rsp、rflags 的现场 */ void *kstack; /* 私有内核栈系统调用时用 */ void *ustack; /* 私有用户栈线程之间不能共用 */ int tid; struct proc *proc; /* 指回所属进程 */ struct thread *next; /* 挂调度队列用 */ }; struct proc { pagetable_t *pgdir; /* 共享页表切线程时完全不换 */ struct file *ofile[NOFILE]; /* 共享打开文件表 */ struct inode *cwd; /* 共享当前工作目录 */ struct sigaction sigact[NSIG];/* 共享信号处理方式 */ int pid; };判断依据非常机械pgdir在proc里所以两个线程访问同一个虚拟地址会落到同一个物理页堆上的全局变量天然互相可见ofile在proc里所以一个线程close掉的文件另一个线程再读就是EBADF。而寄存器现场必须在thread里因为 PC 决定下一条执行哪条指令、SP 决定往哪个栈压数据这两样一旦共享两个线程就会跑同一条指令流、压同一个栈直接崩。顺着这个结构看就明白题目问的是哪个字段的归属地址空间、打开文件、记账信息都在进程结构里只有寄存器是线程私有的。2.3 三级调度各管哪一段短期调度short-term schedule就是 CPU 调度从就绪队列里挑一个进程上处理器频率最高毫秒甚至微秒级一次它的决策必须极快所以只看优先级和时间片不做任何重量级判断。这也是为什么题目把 short term schedule 对应到 CPU scheduling——它决定的是「此刻谁用 CPU」不是「谁被创建」也不是「谁被换出」。长期调度long-term schedule作业调度决定哪个作业被选中、创建成进程进入内存它控制的是多道程序度。批处理系统里这个度调得太高每个进程分到的时间片就碎交互体验崩调得太低CPU 又闲。中期调度medium-term schedule内存调度管的是换出换入内存紧张时挑一个进程挂起到磁盘等内存宽裕再换回来对应进程状态图里那个「挂起」态。三级调度的分工本质是三种不同时间尺度的资源CPU 是微秒级的内存是毫秒到秒级的作业吞吐是分钟级的。用同一套逻辑管三种尺度必然有一头顾不好。2.4 系统调用到底走了哪几步Part 3 第一题问系统调用标准答法是「内核提供给用户的一组接口」但真正体现理解的是那条路径。用户态把系统调用号放进rax参数依次放rdi、rsi、rdx、r10、r8、r9然后执行syscall指令CPU 依据 MSR 里预先登记的入口地址跳到内核同时完成栈切换和rip/rflags保存内核拿rax去查分派表。/* 系统调用分派表用户态那条 syscall 指令最终落到这里 */ static long (*sys_call_table[])(struct trapframe *) { [SYS_read] sys_read, [SYS_write] sys_write, [SYS_fork] sys_fork, [SYS_open] sys_open, }; void syscall_entry(struct trapframe *tf) { if (tf-rax NR_SYSCALLS || !sys_call_table[tf-rax]) tf-rax -ENOSYS; else tf-rax sys_call_table[tf-rax](tf); /* 返回值回填 rax由 sysret 带回用户态 */ }tf-rax在这里身兼两职进来时是调用号出去时是返回值。这也是「一次系统调用两次特权级切换」说法的由来——一次陷入、一次返回中间那次查表分派才是真正的业务。需要额外注意的是参数里的用户态指针在进入内核后必须校验并拷贝copyin否则用户传一个内核地址进来内核就替它读了不该读的内存。3. 分页与地址翻译31 位地址是怎么被切开的3.1 位宽是唯一的信息源题目给了一张图31 到 10 位是页号9 到 0 位是页内偏移。它没告诉你页大小也没告诉你页数全靠位宽反推。偏移占 10 位意味着页内能寻址 2^10 1024 字节页号占 22 位意味着最多 2^22 4M 个页。选 B。def decode(addr_bits, offset_bits): page_size 1 offset_bits # 页大小 2^偏移位数 pages 1 (addr_bits - offset_bits) # 页数 2^页号位数 return page_size, pages print(decode(32, 10)) # (1024, 4194304) - 1K 页、4M 页 print(decode(32, 11)) # (2048, 2097152) - 2K 页、2M 页两个参数的含义必须说清addr_bits是虚拟地址总位宽offset_bits是低位偏移占的位数。分页系统里页大小永远是 2 的幂正是为了让「除法和取余」退化成「移位和掩码」——硬件不做除法右移 10 位得页号与0x3FF得偏移。很多人在这里翻车是因为看到「2M pages at most」就以为是页大小的暗示其实 4M 页对应的是 32 位地址减 10 位偏移加减法算错一位四个选项全像对的。3.2 TLB 打在地址翻译路径上的补丁分页的代价藏得很深页表本身在内存里一次虚拟地址翻译要先读一次页表项再读一次目标数据。也就是说一条普通的mov在开了分页之后变成两次访存性能直接腰斩。TLBTranslation Lookaside Buffer就是针对这条路径的缓存它放在 MMU 里是一组小容量的相联存储器表项结构大致是struct tlb_entry { unsigned long vpn : 40; /* 虚页号命中比较时当 tag 用 */ unsigned long ppn : 24; /* 物理页框号命中后直接拼出物理地址 */ unsigned long asid : 16; /* 地址空间标识避免切换进程就全表刷掉 */ unsigned int valid : 1; unsigned int dirty : 1; /* 该页被写过换出时要回写磁盘 */ unsigned int prot : 3; /* 读 / 写 / 执行权限 */ };命中时MMU 用虚页号做一次并行比较直接拿到物理页框号省掉那次页表访存未命中才走页表遍历并把结果回填。asid字段是后加的补丁早期 x86 没有 PCID一写 CR3 切页表整个 TLB 就得刷掉切换进程后前几百条指令全是冷启动的缺页式延迟。加上地址空间标识之后不同进程的翻译结果可以共存切换只换一个标识。为什么几十条表项就能覆盖工作集因为页访问的局部性比指令级局部性更粗一个程序在一个时间窗口里只会碰少数几个页TLB 装得下。这也是虚拟内存和 TLB 能互相成全的原因。3.3 局部性原理不是「顺序执行」选择题里「程序按顺序执行代码」是个漂亮的干扰项。局部性讲的是两件事时间局部性——刚被访问的指令或数据很可能马上再被访问循环体、计数器变量空间局部性——访问了地址 xx 附近的地址很快也会被访问数组遍历、顺序取指。它描述的是一种「访问概率分布不均」不是「执行流线性」。虚拟内存整套机制建立在这个前提上进程可以声明 8 页的地址空间物理内存只给 4 个帧靠换入换出把当前工作集留在内存里。如果程序真的均匀随机访问整个地址空间命中率会低到每次访问都缺页系统直接颠簸到不可用。Denning 提出的工作集模型就是把这件事量化给一个时间窗口 τ窗口内被访问的页集合就是工作集操作系统只要保证工作集在内存里缺页率就压得住。3.4 页故障与地址范围把除法做对就行题目给的条件是页大小 2K、物理内存 4 个帧、进程需要 8 页页表里第 0、1、3、5 页无效。虚拟页 i 覆盖的地址范围就是[i*2048, (i1)*2048 - 1]虚页有效位物理帧虚拟地址范围0No—0 – 20471No—2048 – 40952Yes14096 – 61433No—6144 – 81914Yes38192 – 102395No—10240 – 122876Yes012288 – 143357Yes214336 – 16383翻译一个具体地址只做两步整除得页号取余得偏移。8500 除以 2048 得 4 余 308页 4 有效且映射到帧 3物理地址就是 3×2048308 64521400 落在页 0 的区间页 0 无效直接缺页5000 是页 2 偏移 904帧 1物理地址 29522100 是页 1 偏移 52缺页。PAGE_SIZE 2048 page_table {0: None, 1: None, 2: 1, 3: None, 4: 3, 5: None, 6: 0, 7: 2} def translate(va): vpn, off divmod(va, PAGE_SIZE) # 硬件等价于 va 11 与 va 0x7FF frame page_table[vpn] if frame is None: return fpage fault vpn{vpn} return fpa{frame * PAGE_SIZE off} print(translate(8500), translate(1400), translate(5000), translate(2100))真正的缺页处理比这个函数长得多陷入内核、查页表确认确实无效、挑一个牺牲帧、脏页回写、从磁盘读入目标页、更新页表、把有效位置 1、重新执行那条触发缺页的指令。注意最后一步是「重新执行」而不是「继续执行下一条」——因为陷入时那条指令的副作用还没发生必须原样重放这也是缺页处理必须可重入的原因。4. 并发与死锁P/V、管程和安全序列的实现细节4.1 信号量的原子性靠哪条指令信号量 S 是个整数但只能被 P 和 V 两个原语碰。P 的语义是「减一如果减完小于零就阻塞自己」V 是「加一如果加完小于等于零就唤醒一个等待者」。注意那个「小于零」而不是「小于等于零」S 为负时绝对值恰好等于等待队列长度这个设计让一个整数同时承载了「剩余资源数」和「等待者数量」两个信息。void P(sem_t *s) { disable_interrupts(); /* 单核上用关中断拿到最便宜的原子性 */ s-value--; if (s-value 0) { enqueue(s-waitq, current); /* 挂到等待队列状态置为阻塞 */ enable_interrupts(); schedule(); /* 让出 CPU被 V 唤醒后从这里返回 */ } else { enable_interrupts(); } } void V(sem_t *s) { disable_interrupts(); s-value; if (s-value 0) { wakeup(dequeue(s-waitq)); /* 只唤醒不立刻切换过去 */ } enable_interrupts(); }单核上关中断够了多核上不够——disable_interrupts只屏蔽当前核心别的核心照样能同时执行 P。所以现代实现靠硬件原子指令x86 的lock xadd、lock cmpxchgARM 的ldrex/strex或 LSE 的ldadd用一条不可被打断的读改写完成「减一并判断」。这也是为什么信号量这类原语必须和内核配合原子性归硬件阻塞和唤醒归内核两者缺一不可。4.2 管程把互斥藏进了语法管程是一组过程、共享变量和初始化代码打包成的模块外加一个隐含的互斥锁。进程可以随时调用管程里的过程但同一个管程里任意时刻只能有一个进程在活动这个互斥由编译器或运行时自动加上不需要调用者写一行 P/V。对写并发代码的人来说这是从「自己管锁」到「结构上保证互斥」的跨越。条件变量是管程里的等待队列只有wait和signal两个操作。wait的关键在于「挂起自己」和「释放管程互斥锁」必须是原子的——如果先放锁再挂起中间那段窗口里别的进程可能已经signal过来信号就丢了自己再也醒不过来。所以pthread_cond_wait(cond, mutex)的第二个参数必须传管程锁由运行时保证这两步不可分割。signal的语义有两种流派Hoare 语义是唤醒者立刻把管程让给被唤醒者被唤醒者马上运行Mesa 语义是唤醒者继续跑完被唤醒者只进就绪队列。pthread 和 Java 都是 Mesa 语义所以被唤醒后条件可能又被别人改掉了——这就是必须用while重新检查条件的根本原因它跟「虚假唤醒」是同一件事的两面。4.3 生产者消费者那六个空这段代码用的是单槽缓冲区buffer 0表示空非 0 表示满并存着数据。把六个空还原出来void *producer(void *arg) { for (int i 1; i MAX; i) { pthread_mutex_lock(the_mutex); while (buffer ! 0) /* ① 槽满生产者等 */ pthread_cond_wait(condp, the_mutex); buffer i; pthread_cond_signal(condc); /* ② 有货了通知消费者 */ pthread_mutex_unlock(the_mutex); /* ③ 放锁 */ } return NULL; } void *consumer(void *arg) { for (int i 1; i MAX; i) { pthread_mutex_lock(the_mutex); while (buffer 0) /* ④ 槽空消费者等 */ pthread_cond_wait(condc, the_mutex);/* ⑤ 等在生产者的对立条件上 */ int res buffer; buffer 0; pthread_cond_signal(condp); /* 通知生产者可以填了 */ pthread_mutex_unlock(the_mutex); /* ⑥ 放锁 */ printf(buffer%d\n, res); } return NULL; }⑤ 填condc而不是condp是最容易错的一处消费者必须等在自己「可能被满足」的那个条件变量上生产者 signal 的也必须是这个变量两边对不上就是永久阻塞。② 和 ③ 的顺序值得多说一句。理论上先 signal 再 unlock 是安全的pthread 允许持锁唤醒但如果先 unlock 再 signal中间会出现一个窗口另一个生产者抢进来把 buffer 填满然后才 signal被唤醒的消费者醒来发现条件已经不成立。while能兜住这种情况代价是一次空转。稳妥写法是持锁 signal、解锁收尾。4.4 银行家算法安全性检查就是一次模拟Need Max - Allocation这一步只是减法真正的逻辑在安全性检查假设所有进程此刻都停住手里资源都不还只有 Available 可用然后找一个 Need 不超过 Available 的进程让它跑完把它手里的 Allocation 全部归还Work 变大再找下一个。找不到可满足的进程而还有进程没完成就判定不安全。int safe p a hrefhttps://download.csdn.net/download/m0_51765271/86698951 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p