第一章 温故而知新
- 硬件历史变化
- 多道程序(Multiprogramming)设计
- 内存
计算机硬件三个关键部件:中央处理器 CPU,内存和 I/O 控制芯片
硬件历史变化
- 早期的计算机硬件:每个设备的 I/O 控制器和 CPU 以及内存连同一总线
- CPU 核心频率提升,产生了与内存频率一致的系统总线
- 为了些协调 CPU、内存和高速的图形设备,设计高速的北桥芯片,以便其高速地交换数据
- 设计南桥芯片,专门处理低速设备(磁盘、USB、键盘、鼠标等),由南桥汇总连到北桥
- 系统总线用 PCI 结构,低速设备用 ISA 总线
- CPU 频率到达 4GHz 的极限,通过增加 CPU 的数量来增加 CPU 的速度
- 对称多处理器(SMP,Symmetrical Multi-Processing),每个 CPU 在系统中所处的地位和发挥的功能一样,是相互对称的
- 应用场合是商用的服务器和需要处理大量计算的环境
- 多核处理器(Multi-core Processor),多个处理器“被打包”,共享比较昂贵的缓存部件
- 系统软件:管理计算机本身的软件
- 平台性的:操作系统内核/驱动程序/运行库和其他系统工具
- 用于程序开发的:编译器/汇编器/链接器等开发工具和开发库
- 计算机软件体系结构:硬件——硬件规格——操作系统内核——系统调用接口——运行时库——操作系统 API——应用软件
- 硬件规格(Hardware Specification),由硬件生产厂商负责提供
- 系统调用接口(System Call API),往往以软件中断(Software Interrupt)的方式提供
- 操作系统应用程序编程接口(API,Application Programming Interface)
多道程序(Multiprogramming)设计
- 充分利用 CPU 资源
- 当某个程序暂时不适用 CPU 时,监控程序把正在等待 CPU 资源的程序启动
- 缺点:调度时不能满足需要即时响应的需求
- 分时系统(Time-Sharing System):每个程序运行一段时间以后都主动让出 CPU 给其他程序
- 缺点:当某个程序进入死循环,整个系统无法响应交互式需求
- 多任务系统(Multi-tasking System):操作系统管理所有硬件资源,所有应用程序以进程的方式运行在比操作系统权限更低的级别
- 每个进程拥有独立的地址空间,进程之间的地址空间互相隔离
- 操作系统统一分配 CPU,根据每个进程的优先级分配
- 进程运行时间超出一定阈值,操作系统会暂停该进程,将 CPU 资源分配给其他等待运行的进程,即抢占式(Preemptive),操作系统可以强制剥夺 CPU 资源并且分配给它认为目前最需要的进程
- 硬件驱动(Device Driver)程序:和操作系统内核一起运行在特权级,但与操作系统内核直接有一定的独立性。将硬件抽象成一系列概念,提供统一的硬件访问模式。
- 驱动程序的开发工作通常由硬件生产厂商完成。
- 操作系统开发者提供一系列接口和框架,硬件生产厂商安装该接口和框架开发的驱动程序都可以在操作系统上使用。
内存
- 早期计算机的程序运行在物理内存,即使用物理地址空间(Physical Address Space)
- 问题:地址空间不隔离;内存使用效率低;程序运行的地址不确定
- 虚拟地址空间(Virtual Address Space):每个进程都挺自己独立的虚拟空间,且只能访问自己的虚拟地址空间
- 分段(Segmentation):分段对内存区域的映射仍以程序为单位,内存不足时内存的切换以程序为单位,会影响速度
- 分页(Paging):把地址空间等分成固定大小的页,每页大小由硬件决定,或硬件支持多种大小的页,由操作系统选择决定页的大小。
- 虚拟页(VP,Virtual Page)
- 物理页(PP,Physical Page)
- 磁盘页(DP,Disk Page)
- 页错误(Page Fault),进程需要的虚拟页不在内存中
- 内存管理单元(MMU,Memory Management Unit)进行页映射,将虚拟地址映射到物理地址
线程(Thread)
- 程序执行流的最小单元,也被称为轻量级进程(LWP,Lightweight Process)
- 线程包含线程 ID,当前指令指针 PC,寄存器集合和堆栈
- 进程由一到多个线程组成,各个线程共享程序的内存空间(代码段、数据段、堆等)及一些进程级的资源(如打开文件和信号)
线程调度(Thread Schedule)
- 三种状态:
- 运行 Running:线程正在执行
- 就绪 Ready:线程可以执行,等待 CPU 调度
- 等待 Waiting:线程正在等待某一事件(I/O或同步),无法执行
- 时间片(Time Slice)
- 优先级调度(Priority Schedule):线程有各自的优先级,优先级高的线程会更早地执行
- IO 密集型线程(IO Bound Thread)比 CPU 密集型线程(CPU Bound Thread)更容易得到优先级的提升
- IO 密集型:频繁等待
- CPU 密集型:频繁进行大量计算
- 系统会提升等待过长时间而未被调度执行的线程的优先级,避免饿死(Starvation)
- 改变线程优先级的三种方式
- 用户指定优先级
- 根据进入等待状态的频繁程度提升或降低优先级
- 长时间得不到执行而被提升优先级
- 轮转法(Round Robin):让各个线程轮流执行一小段时间
- 抢占(Preemption)
Linux 多线程
- 创建线程的三种方式:
- fork 复制当前进程,子进程和父进程共享内存空间,执行相同代码,内存空间是写时复制(COW,Copy On Wright)
- exec 使用新的可执行映像覆盖当前可执行映像
- clone 创建一个线程并从指定位置开始执行
线程安全
- 原子(Atomic)操作:单指令操作,不会被调度系统打断
- 同步(Synchronization):一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问
- 锁(Lock):线程在访问数据或资源之前先试图获得(Acquire)锁,访问结束之后释放(Release)锁
- 信号量(Semaphore):一个初始值为 N 的信号量允许 N 个线程并发访问
- 二元信号量(Binary Semaphore):只有占用和非占用两种状态,适合只能被一个线程独占访问的资源
- 访问资源过程:获取信号量;信号量值减 1;信号量值小于 0,则等待,否则继续执行
- 访问之后释放资源过程:获取信号量;信号量值加 1;信号量值小于 1,则唤醒一个等待线程
- 互斥量(Mutex):只允许一个线程独占访问资源
- 信号量可以被任意线程获取并释放,即获取和释放线程可以不同
- 互斥量只能由获取的线程释放锁
- 临界区(Critical Section):获取临界区的锁即进入临界区,释放锁则离开临界区
- 互斥量和信号量在系统的任何进程都是可见的
- 临界区的作用范围仅限于本进程,其他进程无法获取该锁
- 读写锁(Read-Write Lock):允许多个线程读,一个线程写
- 共享的(Shared):处于共享状态获取的锁,允许其他线程以共享方式获取,此时该锁被分配给多个线程
- 独占的(Exclusive):处于独占状态获取的锁,其他线程不能获得该锁
- 条件变量(Condition Variable)
- 线程可以等待条件变量,一个条件变量允许多个线程等待
- 线程可以唤醒条件变量,此时某个或者所有等待此条件变量的线程都会被唤醒并继续执行
- 可重入(Reentrant)
- 函数被重入,表示这个函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行
- 多个线程同时执行该函数
- 函数自身调用自身
- 函数可重入:函数被重入之后不会产生任何不良后果
- 不使用(局部)静态或全局的非 const 变量
- 不使用(局部)静态或全局的非 const 变量的指针
- 仅依赖调用者提供的参数
- 不依赖单个资源的锁(mutex 等)
- 不调用不可重入的函数
- 过度优化:编译器会保存变量到寄存器,执行程序时可能会交换指令的顺序
- volatile 关键字阻止过度优化
- 防止编译器为了提高速度将一个变量缓存到寄存器而不写回
- 防止编译器跳转操作 volatile 变量的指令顺序
- barrier 指令:阻止 CPU 将该指令之前的指令交换到该指令之后
线程模型
- 一对一:一个用户线程对应一个内核线程
- 真正意义的线程并发
- 对内核线程的数量限制会限制用户线程的数量
- 内核线程上下文切换开销大,导致用户线程的执行效率下降
- 多对一:多个用户线程对应一个内核线程
- 高效的上下文切换
- 不限制用户线程数量
- 一个用户线程阻塞,所有线程无法执行
- 多对多:多个用户线程对应多个内核线程
- 一个用户线程阻塞不会阻塞所有用户线程
- 不限制用户线程数量