本次会议对操作系统课程相关内容进行复习与讲解,涵盖多进程环境下内存管理、系统权限管理、进程间通信及线程等知识,还提及课程实验安排,内容如下:

  • 多进程环境下内存管理复习

  • 上节课内容回顾

  • 内存管理章节重点:当前处于内存管理章节,需构建比实际物理内存大的虚拟地址空间,让程序员误以为有足够内存。假设要知道程序访存轨迹,将所需页面放入物理内存,但目前无硬件能准确追踪访存轨迹。

  • 访存轨迹获取难题:宫晓利认为,一是 cache 的存在使内存无法感知最热数据访问;二是 CACHE line 一次搬运 64 字节,后续对数组其他访问内存无法感知;三是指令 reorder 导致代码访存轨迹与硬件访存轨迹不同。

  • MMU 记录访问:MMU 在地址翻译时能记录以页为单位的访问,这是目前能获取的最好的内存访问行为记录。将访问记录在页表项上,是内存管理的硬件基石,基于此产生了 clock 算法,并结合 dirty bit 作为上一代内存管理算法的收尾。

  • 现代操作系统内存管理

  • 工作集与常驻集概念:现代操作系统需精确为进程分配物理内存,引入工作集和常驻集概念。工作集指程序在某阶段运行要访问的内存量,常驻集指该阶段分配给程序的物理内存量。工作集是相对概念,通常需限定时间范围确定。

  • 工作集猜测问题:目前学术上尚未妥善解决程序工作集的猜测问题。理论上结合编译和体系结构知识可较好预测,但仍需探索。人工智能时代为此提供了机会。

  • 缺页率算法:通过观测缺页率判断进程资源是否充足。若缺页频繁,增加内存;若很久不缺页,回收部分内存。Linux 采用 MGLRU 算法,多次扫描 access bit 为空时置换页面。

  • 段机制废弃:除 Windows 和英特尔处理器外,其他处理器不再支持段机制,该机制因硬件实现省事而被废弃。

  • 进程地址空间独立:MMU 作为传令官,确保进程地址空间独立。Linux 用 fork 和 exec 管理进程,采用半深半浅拷贝方式。进程过多会导致内存争抢,出现抖动现象,可通过给内存添加属性解决。

  • 系统权限管理讲解

  • 信息安全问题引入

  • 早期操作系统漏洞:多道程序运行时,程序可能访问其他程序内存,早期 DOS 系统存在此类恶意代码,硬件工程师为此做了努力。

  • 断长度寄存器作用:早期硬件的断长度寄存器可检查程序是否越界访问,若越界,程序会被停掉或地址被折回。

  • 虚拟内存与进程隔离:现代操作系统采用虚拟内存,每个进程有独立的虚拟地址空间和页表,只要操作系统分配页表时保证不重叠,进程间就无法相互访问。

  • 内核与用户空间划分

  • 空间分布情况:操作系统设计者将进程核心数据存于虚拟地址空间的高地址部分(3G - 4G),低地址部分(0 - 3G)留给用户使用。

  • 数据保护难题:应用程序开发者可能试图修改内核数据,C++ 的类型保护是编译时保护,Java 虽有运行时保护但仍可被入侵,最终需硬件解决。

  • 硬件权限管理机制

  • 特权等级与资源状态:硬件提供四种特权等级,实际常用 0 和 3 两级,分别对应特权态和用户态。资源分为特权内存和常规内存。

  • 权限检查与异常处理:MMU 在访存时进行权限检查,若权限不匹配,访存会被阻断并产生缺页异常。CPU 用专用寄存器保存当前特权等级,内存页表用专用比特标记页面特权属性。

  • 权限提升模式

  • 中断:如时钟中断到来时,CPU 权限从用户态提升到特权态,跳转到操作系统预设的中断向量表入口执行代码,执行完后回收特权返回。

  • 异常:程序出现除零、段错误等异常时,CPU 抛出异常,操作系统捕获后处理,通常会杀死出错进程。

  • 系统调用:应用程序需操作系统帮忙做特殊权限的事时,通过系统调用实现。操作系统与应用程序约定 syscall number,应用程序触发中断提升权限,将约定的系统调用号填入寄存器,操作系统根据号码执行相应操作,完成后回收权限。

  • 系统调用详细介绍

  • 系统调用原理

  • 约定与实现方式:操作系统和应用程序通过 syscall number 约定可做的事情,应用程序触发中断提升权限,将系统调用号填入寄存器,操作系统根据号码执行操作。

  • 不同体系结构实现:以 X86 为例,通过 INT 80 指令触发中断,将系统调用号填入 ax 寄存器;RISC five 用 ECALL 指令,将系统调用号填入 A0 寄存器;arm 用 SVC 指令。

  • 安全性检查:系统调用实现代价高,需进行复杂的安全性检查,确保用户提供的参数合法。现代操作系统内核代码中大部分用于安全性检查。

  • 系统调用的封装与应用

  • 库函数封装:为方便使用,系统调用被封装成程序设计库,如 C++ 库中的 cout、C 库中的 printf 等。

  • 接口稳定性:系统调用接口不能随意更改,不同体系结构和 Linux 版本的系统调用号和功能略有差异。

  • 信创产业挑战:信创产业在移植开源代码时,因操作系统实现的 Syscall 不同和汇编指令不兼容,会出现问题。

  • 系统调用的新思考

  • 应用程序接口设想:对于智能手机应用,设想让应用程序开发者暴露类似 Syscall 的接口,以提高用户服务效率,但这与商家利益存在矛盾。

  • 系统调用总结:普通应用程序运行时,操作系统大部分时间不占用 CPU,在中断、异常和系统调用时,PC 会跳转到内核指令执行,执行完后返回。

  • 进程间通信讲解

  • 进程间通信需求提出

  • 进程隔离与通信矛盾:进程间相互隔离,但存在数据交换需求,如复制粘贴操作。

  • 内核空间共享基础:每个进程有用户态和内核态空间,内核态空间部分共享,为进程间通信提供基础。

  • 进程间通信模式

  • 信号:一个进程通知另一个进程事件发生,如关机、暂停等。每个进程有信号缓冲区,操作系统将信号标记放入缓冲区,进程获得运行权时检查标记并响应。系统中信号数量有限,常用信号已被操作系统占用,用户可重定义 user 1 和 user 2 信号。

  • 管道:用于两个已知进程间通信,在内核开辟固定大小空间(通常 4K),表现为一个读文件和一个写文件,数据按先入先出顺序传递。

  • 消息队列:在内核开辟空间存放数据结构数组,使用时先通过 message get 预留空间,获取 queue ID,通信双方通过该 ID 发送和接收消息,常用于键盘、鼠标等窗口事件分发。

  • 共享内存:通过系统调用 share memory get 申请物理页,attach 到进程地址空间,实现两个进程间最高效的数据通信。使用时需注意数据格式约定和内存回收问题,避免出现 use after free 等安全漏洞。

  • 线程概念讲解

  • 线程的产生背景

  • 多程序交互需求:以坦克大战游戏为例,多个程序需频繁交互,传统方式难以满足,引入线程概念。

  • 线程的定义与实现:线程是交替执行的代码执行体,早期由用户态函数库模拟实现,后操作系统提供支持。

  • 线程的特点与优势

  • 上下文与调度:操作系统为线程创建上下文,记录函数执行位置。用户态线程调度点需开发者自己编写,内核态线程由操作系统按时间片轮转等方式调度。

  • 执行速度比较:若线程不发生阻塞,用户态线程执行速度快;若有线程可能触发阻塞系统调用,内核态线程更快。

  • 线程的应用场景

  • 现代操作系统模式:现代操作系统采用多进程和多线程结合的运行模式,每个进程有多个线程,如游戏中的 NPC、技能特效等可由不同线程实现。

  • 线程模型研究:曾有研究探索内核上下文与用户态函数的复杂对应关系以优化系统性能,但因过于复杂未被广泛应用。