Back

[中文] Operating Systems Notes: 03 - 进程线程模型[中文] Operating Systems Notes: 03 - 进程线程模型

1. 核心问题解答#

  1. 怎样理解“进程是对CPU的抽象”这句话?

    • 解答: 物理CPU只有一个(或有限个),但通过多道程序设计和操作系统的进程调度,可以让多个程序在宏观上“同时”运行。操作系统为每个运行的程序创建一个进程,并管理它们对CPU的使用(分时复用)。这使得每个进程都感觉自己仿佛独占了一个CPU(或一个虚拟CPU)来执行自己的指令序列。因此,进程机制将一个或多个物理CPU虚拟化成了多个虚拟CPU,供多个程序并发执行,这是对CPU计算能力的抽象。
  2. 何谓进程映像?进程有实体吗?在哪里?

    • 解答: 进程映像 (Process Image) 是指进程在执行时的完整状态描述,是进程实体的静态体现。它包括:
      • 程序代码 (Code Segment): 进程要执行的指令。
      • 程序数据 (Data Segment): 程序使用的全局变量、静态变量等。
      • 程序堆栈 (Stack): 用于函数调用、局部变量存储。
      • 堆 (Heap): 动态分配内存的区域。
      • 进程控制块 (PCB): 包含进程的所有管理信息(状态、ID、寄存器值、资源列表等)。
    • 进程是有实体的。它的实体就是进程映像所包含的内存区域(代码、数据、堆栈、堆)以及在操作系统内核中的数据结构(PCB)。这些实体主要存在于 内存 中(代码、数据、堆栈、堆)和 操作系统内核空间(PCB及其相关数据结构)。当进程被挂起时,部分映像可能被交换到 磁盘(交换空间) 上。
  3. 怎样描述进程?一个进程都有什么(组成要素)?

    • 解答: 描述一个进程主要通过其 进程控制块 (PCB)。PCB是操作系统感知进程存在的唯一标志,包含了描述和控制进程运行所需的所有信息。
    • 一个进程的组成要素(即进程映像)包括:
      • 程序代码
      • 数据集合 (全局变量、静态变量、动态分配的内存)
      • 执行上下文 (CPU寄存器值、程序计数器PC、程序状态字PSW、栈指针等)
      • 进程控制块 (PCB) (包含进程标识符、状态、优先级、资源列表等)
  4. 创建进程主要完成哪些工作?

    • 解答: 创建一个进程(例如通过 fork()CreateProcess)主要包括:
      • 分配进程标识符 (PID): 给新进程一个唯一的ID。
      • 创建和初始化进程控制块 (PCB): 分配PCB结构,并填入初始信息(如PID、父进程ID、初始状态设为New或Ready、优先级等)。
      • 分配地址空间: 为进程分配独立的虚拟内存空间(可能通过复制父进程空间或加载新程序)。
      • 加载程序和数据: 将可执行文件的代码和数据加载到进程的地址空间中(exec 的工作)。
      • 初始化执行上下文: 设置PC指向程序入口,初始化栈指针和寄存器。
      • 分配资源: 分配进程所需的其他资源(如文件描述符,继承自父进程或新创建)。
      • 状态设置与调度: 将进程状态设置为就绪态 (Ready),并将其链入就绪队列,等待调度器分配CPU。
  5. 进程的生命周期内都会经历哪些变化?怎样表示这些变化?

    • 解答: 进程在其生命周期中会经历状态的转换。基本的状态包括:
      • 创建态 (New): 进程正在被创建。
      • 就绪态 (Ready): 具备运行条件,等待CPU。
      • 运行态 (Running): 正在CPU上执行。
      • 等待态/阻塞态 (Waiting/Blocked): 等待某个事件(如I/O完成)而暂停执行。
      • 终止态 (Terminated): 进程执行完毕或被终止,等待系统回收资源。
    • 这些变化通常用 进程状态转换图 来表示,图中的节点代表状态,有向边代表状态之间的转换及其触发条件(如调度、等待事件、事件完成等)。
  6. 进程有哪些状态?进程状态之间的转换(条件?操作?)

    • 解答:
      • 基本状态: 运行态 (Running)、就绪态 (Ready)、等待态 (Waiting/Blocked)。
      • 其他状态: 创建态 (New)、终止态 (Terminated)。还可能引入挂起态 (Suspended Ready, Suspended Blocked)。
      • 常见转换及条件/操作:
        • New -> Ready: OS完成进程创建的必要工作,资源基本到位,允许参与调度。
        • Ready -> Running: 进程被调度器 (Scheduler) 选中,获得CPU使用权。操作: 上下文切换,恢复进程现场。
        • Running -> Ready: 时间片用完;或被更高优先级的进程抢占。操作: 上下文切换,保存进程现场。
        • Running -> Waiting: 进程请求I/O操作或等待某一资源/事件。操作: 进程主动调用阻塞原语 (e.g., wait()),保存现场,移入等待队列。
        • Waiting -> Ready: 进程等待的事件发生或资源可用(如I/O完成)。操作: 中断处理程序或相关内核线程执行唤醒原语 (e.g., wakeup()),将进程移入就绪队列。
        • Running -> Terminated: 进程正常执行完毕或出错退出。操作: 进程调用退出原语 (e.g., exit()),进入终止态。
        • Terminated -> Gone: OS回收进程所占资源(PCB、内存等)。
  7. 进程状态转换的发生,是否一定导致另一个转换发生?

    • 解答: 是的,通常是这样。 操作系统是一个动态系统,进程状态转换往往是相互关联的。
      • 例如,一个进程从 Running -> Waiting,会释放CPU,这使得调度器可以选择另一个处于 Ready 状态的进程,使其发生 Ready -> Running 的转换。
      • 一个进程从 Waiting -> Ready (如I/O完成),它进入就绪队列,可能在未来某个时刻引发 Ready -> Running 的转换(当它被调度时)。
      • 一个进程 Running -> Terminated,会释放它占有的资源,这可能使得另一个 Waiting (等待该资源) 的进程变为 Ready
  8. 操作系统给进程提供内存空间,该空间的地址是虚拟地址还是物理地址?为什么?

    • 解答: 操作系统提供给进程的地址空间是 虚拟地址空间 (Virtual Address Space)
    • 原因:
      • 隔离与保护: 每个进程拥有独立的虚拟地址空间,一个进程无法直接访问另一个进程的内存,提供了安全保护。
      • 地址空间扩展: 虚拟地址空间可以大于物理内存,借助内存管理单元(MMU)和磁盘交换空间,给进程提供更大的可用地址范围。
      • 内存管理简化: 操作系统可以更灵活地管理物理内存,例如将非连续的物理内存页映射到连续的虚拟地址空间,简化了内存分配和程序加载。
      • 程序加载和链接简化: 程序可以在编译链接时确定其在虚拟地址空间的布局,而无需关心实际加载到物理内存的哪个位置。
  9. 操作系统如何描述进程的地址空间?

    • 解答: 操作系统内核通常使用特定的数据结构来描述进程的地址空间。例如:
      • Linux 中,使用 mm_struct 结构来表示一个进程的整个地址空间。mm_struct 内部包含一个 vm_area_struct (VMA) 的链表或树,每个 VMA 描述了虚拟地址空间中的一个连续区域(段),包括其起止地址、访问权限(读/写/执行)、映射的文件(如果有)等信息。
      • 通过 页表 (Page Tables)段表 (Segment Tables) 将虚拟地址映射到物理地址。这些表由硬件(MMU)使用,操作系统负责维护。
      • 可以通过 cat /proc/<PID>/maps 命令查看一个进程的虚拟地址空间布局和 VMA 信息。需要将 <PID> 替换为实际的进程 ID (可通过 ps 命令查找),否则会提示文件或目录不存在。
  10. 为什么有了进程后又引入线程?

    • 解答: 引入线程主要是为了解决进程的以下不足:
      • 并发应用需求: 许多应用内部包含多个并发执行的任务(如Web服务器处理多个请求,GUI程序响应用户输入同时后台处理)。用多进程实现这些任务,开销较大且通信复杂。
      • 开销问题: 创建进程、撤销进程、以及在进程间切换(上下文切换)都需要较大的时间和系统资源开销。线程作为“轻量级进程”,创建、销毁和切换的开销小得多。
      • 通信效率: 同一进程内的线程共享地址空间和大部分资源,它们之间的通信(通过共享内存)非常高效,无需内核介入。进程间通信 (IPC) 通常需要内核的协调,更复杂且效率较低。
      • 性能提升: 在多核处理器上,同一进程的多个线程可以真正并行执行在不同的核心上,提高应用程序的吞吐量。
  11. 怎样实现线程机制?为什么有各种支持线程的方式?

    • 解答: 线程机制主要有三种实现方式:
      • 用户级线程 (User-Level Threads, ULT): 线程的管理(创建、调度、同步)完全在用户空间由一个线程库来完成。内核对线程无感知,只管理进程。
        • 优点: 切换快(不需内核模式),可自定义调度算法,可移植性好。
        • 缺点: 一个线程阻塞(如系统调用),整个进程会阻塞;无法利用多核并行。
      • 核心级线程 (Kernel-Level Threads, KLT): 线程的管理由操作系统内核完成。内核知道每个线程的存在,并进行调度。
        • 优点: 一个线程阻塞不影响其他线程;可以利用多核并行。
        • 缺点: 线程创建、销毁、切换需要进入内核,开销比ULT大。
      • 混合实现 (Hybrid Implementation): 结合了ULT和KLT。内核管理KLT,用户空间线程库将多个ULT映射到一个或多个KLT上。
        • 目标: 兼具两者的优点,但实现复杂。
    • 存在多种方式的原因: 是在性能、并发能力、实现复杂度、系统资源消耗之间进行权衡的结果。
      • ULT优先考虑低开销和灵活性。
      • KLT优先考虑真正的并发和对阻塞系统调用的处理。
      • 混合模型试图找到一个平衡点。
  12. 线程包Pthreads中相关的函数的功能?

    • 解答: Pthreads (POSIX Threads) 是一个线程API标准,提供了一系列函数来管理线程:
      • pthread_create(): 创建一个新的线程。
      • pthread_exit(): 终止调用该函数的线程。
      • pthread_join(): 等待指定的线程终止。
      • pthread_yield(): 主动让出CPU,让其他线程运行。
      • pthread_self(): 获取调用线程自身的线程ID。
      • pthread_mutex_init(), pthread_mutex_lock(), pthread_mutex_unlock(), pthread_mutex_destroy(): 互斥锁相关操作,用于保护临界区,实现线程互斥。
      • pthread_cond_init(), pthread_cond_wait(), pthread_cond_signal(), pthread_cond_broadcast(), pthread_cond_destroy(): 条件变量相关操作,用于线程间的同步(等待某个条件满足)。
  13. 中断/异常机制与进程线程模型的关联?

    • 解答: 中断和异常是操作系统得以实现进程/线程调度和管理的关键机制。
      • 上下文切换触发: 时钟中断 (Timer Interrupt) 使得操作系统可以剥夺当前运行进程/线程的CPU使用权(时间片用完),进行调度,切换到其他就绪的进程/线程。这是实现分时复用的基础。
      • 状态转换: I/O完成中断会通知操作系统,操作系统可以将等待该I/O的进程/线程从等待态转换为就绪态。
      • 系统调用: 进程通过执行特定的指令(如 syscallint 0x80)产生异常(陷阱 Trap),主动陷入内核态,请求操作系统服务(如创建进程、读写文件、阻塞等待)。内核处理完请求后,可能会进行调度。
      • 错误处理: 异常(如除零、缺页故障 Page Fault)也需要内核介入处理。缺页故障处理是虚拟内存管理的核心部分,可能导致进程阻塞(等待页面从磁盘调入)。
      • 保存与恢复现场: 发生中断/异常时,硬件和操作系统内核协作,必须保存当前进程/线程的执行上下文(寄存器、PC、状态等),处理事件后,再恢复某个进程/线程(可能是同一个,也可能是不同的)的上下文继续执行。
  14. 机制和策略分离的原则在进程线程模型中的体现?

    • 有点像操作系统提供的 API 和调用这些 API 的策略之间的关系
    • 解答: 机制与策略分离 (Separation of Mechanism and Policy) 是操作系统设计的重要原则,意指提供实现某种功能的基础能力(机制),与决定何时、如何使用这些能力的决策逻辑(策略)分开。
      • 进程/线程状态管理(机制) vs. 调度算法(策略): 操作系统提供了进程/线程状态(就绪、运行、等待等)以及在它们之间转换的机制(如阻塞/唤醒原语、上下文切换)。但是,选择 哪个就绪进程/线程投入运行,则是调度算法(策略)决定的(如FIFO、轮转、优先级调度等)。
      • 挂起/激活(机制) vs. 负载调节(策略): 操作系统提供将进程换出到磁盘(挂起)和换回内存(激活)的机制。但是,决定 何时挂起哪个进程(例如,为了降低内存压力或提高系统吞吐量),则是系统负载调节策略的一部分。
      • 线程实现(机制) vs. 应用并发模型(策略): 用户级线程库提供创建和管理线程的机制。应用程序如何 利用这些线程来构建并发逻辑(例如,线程池大小、任务分配方式)则是应用层面的策略。
  15. 协程是什么?为什么引入协程?协程怎么用?

    • 解答:
      • 是什么: 协程 (Coroutine) 是一种比线程更轻量级的用户态并发(或协作式多任务)实现方式。它们是可以在特定点暂停执行,并在稍后从同一点恢复执行的计算过程。协程之间的切换由程序员(或协程库/语言运行时)显式控制,通常发生在用户态,不需要内核介入。
      • 为什么引入:
        • 极低的切换开销: 协程切换完全在用户态进行,避免了内核态和用户态之间的切换以及内核调度,开销远小于线程切换。
        • 高并发能力: 单个线程可以管理成千上万个协程,特别适合处理大量并发连接(如网络服务器)或I/O密集型任务,能有效减少线程数量和内存消耗。
        • 简化异步编程: 允许使用看似同步的代码风格来编写异步逻辑(例如,使用 async/await 关键字),从而避免 回调地狱 (Callback Hell)。回调地狱是指在传统异步编程中,当一个操作依赖于另一个异步操作的结果时,需要将后续操作放在前一个操作的回调函数中,如果存在多层依赖,就会形成层层嵌套的回调函数结构,导致代码难以阅读、理解和维护。
      • 怎么用: 通常通过编程语言或第三方库提供的支持来使用。
        • 语言原生支持: 如 Python (async/await), Go (goroutine), C++20 (co_await, co_yield, co_return), Rust (async/await)。
        • 库支持: 通过特定的协程库在不支持原生协程的语言中使用。
        • 用法: 开发者定义协程函数,在需要等待的操作(通常是I/O)前使用特定关键字 (如 awaityield) 暂停当前协程,让出执行权给其他协程或事件循环,当操作完成后,协程从暂停点恢复执行。

2. 进程模型 (Process Model)#

2.1 基本概念#

  1. 顺序程序与顺序环境 (Sequential Program & Environment)

    • 程序 (Program): 指令或语句的序列,体现某种算法,是静态的。
    • 顺序环境: 系统中只有一个程序在运行,独占所有资源,执行不受外界干扰。
    • 特征:
      • 顺序性: 指令严格按程序规定顺序执行。
      • 封闭性: 程序运行时独占资源,不受外界干扰。
      • 可再现性: 只要输入相同,程序执行结果总是相同,与速度无关。
  2. 多道程序设计 (Multiprogramming)

    • 允许多个程序 同时 进入内存并 交替 运行。
    • 目的: 提高CPU利用率和系统整体效率。当一个程序等待I/O时,CPU可以切换去执行另一个程序。
  3. 并发环境与并发程序 (Concurrent Environment & Program)

    • 并发环境: 一段时间间隔内,单处理器上有两个或以上程序同时处于开始运行但尚未结束的状态,并且执行次序不确定。宏观上并行,微观上串行(在单核CPU上)。
    • 并发程序: 在并发环境中执行的程序。
    • 特征:
      • 间断性: 程序执行走走停停 (执行 -> 停 -> 执行)。
      • 资源共享: 多个程序可能共享系统资源(CPU、内存、I/O设备)。
      • 不可再现性: 由于执行走停的时机和顺序不确定,以及共享资源可能被修改,程序执行结果可能与执行速度有关,变得不可再现。
      • 独立性与制约性: 程序各自独立运行,但也可能因共享资源或需要协作而相互制约。
      • 程序与计算不再一一对应: 一个程序可能对应多次执行(多个进程),一次执行也可能断续完成。
  4. 进程 (Process)

    • 定义:
      • 程序的一次执行过程。
      • 正在运行程序的抽象。
      • 操作系统进行 资源分配调度独立单位
      • 具有独立功能的程序在某个数据集合上的一次运行活动。
    • 进程是对CPU的抽象: 如前所述,它将物理CPU虚拟化为多个逻辑CPU。
    • 资源分配单位: 系统资源(如内存、文件句柄)以进程为单位进行分配。每个进程通常拥有独立的 地址空间
    • 调度单位: 操作系统将CPU时间片调度给进程(或进程中的线程)。
    • 进程与程序的区别:
      • 动态 vs. 静态: 进程是动态的(有生命周期),程序是静态的(文件)。
      • 并发描述: 进程是描述并发的基本单位,程序不能。
      • 生命周期: 进程是暂时的(创建、运行、消亡),程序是相对长久的。
      • 对应关系: 一个程序可以对应多个进程实例。

2.2 进程模型详解#

  1. 进程状态 (Process States)

    • 三种基本状态:
      • 运行态 (Running): 进程占有CPU,并在CPU上运行。
      • 就绪态 (Ready): 进程已具备运行条件(资源到位),但因无空闲CPU而等待。
      • 等待态 (Waiting/Blocked): 进程因等待某一事件(如I/O完成、信号量)而暂时不能运行。
    • 其他状态:
      • 创建态 (New): 进程正在被创建,OS已分配PCB,但尚未完成所有初始化或未被批准执行。
      • 终止态 (Terminated): 进程已停止执行,等待OS回收资源。
      • 挂起态 (Suspended): 进程映像被从内存移到外存(磁盘),用于调节系统负载或用户请求。可以有 挂起就绪 (Suspended Ready)挂起阻塞 (Suspended Blocked) 两种状态。
  2. 进程状态转换模型 (State Transition Models)

    • 三状态模型:

      graph LR
          Ready(就绪) --> |调度|Running(运行)
          Running --> |时间片到/高优先级进程抢占|Ready
          Running --> |等待事件|Waiting(阻塞)
          Waiting --> |事件发生|Ready
      
      mermaid
    • 五状态模型:

      graph LR
          New(创建) --> |提交|Ready(就绪)
          Ready --> |调度|Running(运行)
          Running --> |时间片到/高优先级进程抢占|Ready
          Running --> |等待事件|Waiting(阻塞)
          Waiting --> |事件发生|Ready
          Running --> |完成|Terminated(终止)
      
      mermaid
    • 七状态模型:

      graph LR
          New(创建) --> |提交|Ready(就绪)
          Ready --> |调度|Running(运行)
          Running --> |时间片到/高优先级进程抢占|Ready
          Running --> |等待事件|Waiting(阻塞)
          Waiting --> |事件发生|Ready
          Running --> |完成|Terminated(终止)
          Ready --> |挂起Suspend|SReady(就绪挂起)
          SReady --> |激活Activate|Ready
          Waiting --> |挂起Suspend|SWaiting(阻塞挂起)
          SWaiting --> |事件发生|SReady
          SWaiting --> |激活Activate|Waiting
          New --> |提交|SReady
          Running --> |挂起Suspend|SReady
      
      mermaid
    • Linux 进程状态: 包括 R (TASK_RUNNING) (运行或就绪), S (TASK_INTERRUPTIBLE) (可中断睡眠), D (TASK_UNINTERRUPTIBLE) (不可中断睡眠), T (TASK_STOPPED) (停止), Z (TASK_DEAD - ZOMBIE) (僵尸) 等。其状态模型与理论模型有所差异,更贴近实现。

    • XV6 进程状态: UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE。这是一个简化的教学模型。

    • 不同模型的意义: 体现了 机制和策略分离,基础状态转换是机制,增加挂起等状态是为了实现更复杂的内存管理和负载均衡策略。

  3. 进程控制块 (Process Control Block, PCB)

    • 定义: 操作系统用于管理进程的核心数据结构,是进程存在的唯一标志。也称进程描述符。所有进程的PCB集合构成进程表。
    • 作用: 保存进程状态、资源、上下文等信息,供OS进行调度和管理。
    • 主要内容:
      • 进程描述信息: PID (唯一标识), 进程名, 用户ID (UID), 进程组关系。
      • 进程控制信息:
        • 当前状态 (State)。
        • 优先级 (Priority)。
        • CPU现场信息 (Context): 程序计数器 (PC), 各种CPU寄存器, 程序状态字 (PSW), 栈指针 (SP)。这是进程切换时需要保存和恢复的关键信息。
        • 调度相关信息 (如等待事件、时间片等)。
      • 所拥有的资源和使用情况:
        • 虚拟地址空间描述 (指向页表/段表的指针)。
        • 打开文件列表。
        • I/O设备信息。
      • 进程间通信与同步信息: 消息队列指针, 信号量等。
      • 记账信息: CPU使用时间, 内存使用量等。
    • 具体实现: 不同OS有不同结构,如 Linux 的 task_struct, Windows 的 EPROCESS/KPROCESS/PEB, Solaris 的 proc_t。真实系统中的PCB结构非常庞大复杂。
  4. 进程地址空间 (Process Address Space)

    • 概念: 操作系统为每个进程分配的、独立的 虚拟内存 范围。是对内存的抽象。
    • 典型布局 (从低地址到高地址):
      • 代码段 (.text): 存放程序指令,通常只读。
      • 数据段 (.data, .bss): 存放已初始化的全局/静态变量 (.data) 和未初始化的全局/静态变量 (.bss)。
      • 堆 (Heap): 动态内存分配区域 (malloc, new),向上增长。
      • (文件映射区/共享库): 加载动态链接库、内存映射文件等。
      • 栈 (Stack): 存放函数参数、局部变量、返回地址等,向下增长。
      • 内核空间: 每个进程地址空间的高地址部分映射到操作系统的内核空间,供系统调用和中断处理使用(用户态不可直接访问)。
    • 独立性来源: 每个进程有自己的页表/段表,将相同的虚拟地址映射到不同的物理内存页(或相同的只读页,如共享库代码)。
    • 写时复制 (Copy-on-Write, COW): fork() 创建子进程时,并不立即复制整个地址空间,而是让父子进程共享物理页面,并将页面标记为只读。当任何一方尝试写入时,触发异常,内核才真正复制该页面,使其私有化。这极大地优化了 fork() 的效率,特别是 fork() 后立即 exec() 的情况,因为exec()会替换整个地址空间,使得大部分共享页面在被写入前就已被丢弃,从而避免了不必要的复制开销。
      • COW异常的详细处理流程:
        1. 写操作触发页面故障: 当父进程或子进程尝试写入共享的只读页面时,CPU检测到违反内存保护,触发页面故障异常(page fault)。
        2. 进入内核态: CPU立即切换到内核态,保存当前上下文,并跳转到页面故障处理程序。
        3. 异常处理: 内核的页面故障处理程序检查故障原因,发现是COW页面的写操作。
        4. 页面复制: 内核为写操作进程分配一个新的物理页框,将原共享页面的内容完整复制到新页框中。
        5. 页表更新: 修改发起写操作的进程的页表,将相关虚拟地址映射到新分配的物理页框,并设置为可写权限。
        6. 恢复执行: 内核返回用户态,恢复被中断的进程执行,此时写操作可以正常进行,且不会影响另一进程的内存视图。
    • 查看: cat /proc/<PID>/maps (Linux) 可以查看进程的虚拟内存区域布局。
  5. 进程队列 (Process Queues)

    • 操作系统通常根据进程状态将PCB组织在不同的队列中。
    • 就绪队列 (Ready Queue): 存放所有处于就绪态的进程PCB。调度器从中选择下一个要运行的进程。可能按优先级组织成多个队列。
    • 等待队列 (Waiting Queues): 可能有多个,每个队列对应一个特定的等待事件(如等待磁盘I/O、等待键盘输入、等待某个信号量)。当进程等待某事件时,其PCB被移入相应的等待队列。
    • 进程状态的改变伴随着其PCB在不同队列间的移动。

2.3 进程控制#

进程控制原语#

原语(Primitive)是完成某种特定功能的一段程序,具有不可分割性不可中断性,即原语的执行必须是连续的,在执行过程中不允许被中断,也称为原子操作(Atomic)

进程控制操作完成进程各状态之间的转换,由具有特定功能的原语完成:

  • 进程创建原语
  • 进程撤销原语
  • 阻塞原语
  • 唤醒原语
  • 挂起原语
  • 激活(解挂)原语
  • 改变进程优先级原语
  • 等等

进程的生命周期#

进程创建的时机:

  • 系统初始化时
  • 操作系统提供的服务
  • 交互用户登录系统
  • 由现有的进程派生出一个新进程
  • 提交一个程序执行(例如,命令行)

进程终止的时机:

  • 正常退出(自愿的)
  • 出错退出(自愿的)
  • 严重错误(非自愿)
  • 被其他进程杀死(非自愿)

进程终止的各种事件:

  • 正常结束
  • 给定时限到
  • 缺少内存
  • 存储器出界
  • 保护性出错(写只读文件)
  • 算术错误
  • 超出时间(进程等待超过对某事件的最大值)
  • I/O 失败
  • 无效指令(如试图执行数据)
  • 特权指令
  • 操作系统干预(如当死锁发生时)
  • 父进程请求中止某一子进程
  • 父进程中止(子进程也中止)

进程控制操作#

进程的创建#

进程创建的主要步骤:

  • 给新进程分配一个唯一标识(pid)以及进程控制块(PCB)
  • 为进程分配地址空间
  • 初始化进程控制块
    • 设置默认值(如:状态为 New,…)
    • 设置相应的队列指针(如:把新进程加到就绪队列的链表中)
  • 创建或扩充其他数据结构

不同操作系统的实现:

  • UNIX:fork/exec
  • WINDOWS:CreateProcess
进程的撤销#

进程撤销的主要步骤:

  • 结束子进程或线程
  • 收回进程所占有的资源
    • 关闭打开的文件
    • 断开网络连接
    • 回收分配的内存等
  • 撤销该进程的PCB

不同操作系统的实现:

  • UNIX:exit
  • WINDOWS:ExitProcess
进程阻塞和进程唤醒#

处于运行状态的进程,在其运行过程中期待某一事件发生(如等待键盘输入、等待磁盘数据传输完成、等待其它进程发送消息),当被等待的事件未发生时,由进程自己执行阻塞原语,使自己由运行态变为阻塞态。

不同操作系统的实现:

  • UNIX:wait
  • WINDOWS:WaitForSingleObject
UNIX系统设计的进程控制操作#

UNIX系统提供了一系列系统调用来实现进程控制:

  • fork(): 通过复制调用进程来建立新的进程,是最基本的进程建立过程
  • exec(): 包括一系列系统调用,它们都是通过用一段新的代码覆盖原来的内存空间,实现进程执行代码的转换
  • wait(): 提供初级的进程同步措施,能使一个进程等待,直到另外一个进程结束为止
  • exit(): 用来终止一个进程的运行

这些系统调用之间的关联(shell、fork()、exec()、wait())体现了UNIX进程管理的设计哲学,通过简单而正交的原语组合实现复杂功能。

UNIX的fork()实现及优化#

UNIX的fork()实现步骤:

  • 为子进程分配一个空闲的进程描述符(proc结构)
  • 分配给子进程唯一标识pid
  • 以一次一页的方式复制父进程地址空间
  • 从父进程处继承共享资源,如打开的文件和当前工作目录等
  • 将子进程的状态设为就绪,插入到就绪队列
  • 对子进程返回标识符0
  • 对父进程返回子进程的pid

优化方案: Linux的解决方案是利用存储管理模块中的”写时复制技术”COW(Copy-On-Write)对fork()进行了优化。

写时复制(Copy-on-Write, COW)技术#

重新审视fork函数:

  • 虚拟内存和内存映射解释了fork如何为每个进程提供私有地址空间
    • fork()后跟exec()的常见情况的完美方法
  • 为新进程创建虚拟地址空间的步骤:
    • 创建新的进程mm_struct、vm_area_struct、页表的精确副本
    • 将两个进程中的每个页面标记为只读
    • 将两个进程中的每个vm_area_struct标记为私有COW
    • 返回时,每个进程都有虚拟内存的精确副本
    • 后续写入使用COW机制创建新页面

3. 线程模型 (Thread Model)#

3.1 线程的引入#

  1. 为什么引入线程?
    • 应用的需要: 一个应用程序内部往往有多个并发执行流的需求。例如:
      • 字处理软件: 用户输入(前台线程)、后台自动保存(后台线程)、拼写检查(后台线程)。
      • Web服务器: 主线程监听连接,每个连接分配一个工作线程处理请求(读文件、网络发送)。
    • 开销的考虑:
      • 进程创建、销毁、切换的开销(时间、空间)较大。
      • 线程是轻量级的,其创建、销毁、切换开销小得多。
    • 性能的考虑:
      • 通信效率: 同一进程的线程共享地址空间和资源,通信(共享内存)非常高效,无需内核干预。
      • 并行计算: 在多核CPU上,同一进程的多个线程可以真正并行执行。

3.2 线程的基本概念#

  1. 线程 (Thread)

    • 进程内的一个 执行实体 (或执行流)
    • CPU调度 的基本单位。
    • 有时称为 轻量级进程 (Lightweight Process, LWP)
    • 进程现在被视为 资源分配 的基本单位。
  2. 线程的属性:

    • 拥有独立的状态: (Running, Ready, Blocked等),需要进行状态转换管理。
    • 拥有独立的执行上下文: 程序计数器 (PC), 寄存器集合, 栈 (Stack) 和栈指针 (SP)。线程切换时保存/恢复的是这部分私有上下文。
    • 共享所在进程的资源:
      • 地址空间(代码段、数据段、堆)。
      • 打开的文件。
      • 全局变量。
      • 信号处理器等。
    • 可以创建、撤销、同步其他线程。
  3. 多线程进程模型: 一个进程包含一个PCB和多个线程控制块 (Thread Control Block, TCB)。所有TCB共享进程的地址空间和资源,但每个TCB有自己独立的PC、寄存器和栈。

3.3 线程的实现#

  1. 用户级线程 (User-Level Threads, ULT)

    • 实现: 在用户空间通过线程库实现,内核对线程无感知。线程调度由库函数完成。
    • 优点:
      • 创建、销毁、切换非常快(不涉及内核模式切换)。
      • 调度算法可以由应用程序定制。
      • 可以运行在不支持线程的操作系统上(只需有线程库)。
    • 缺点:
      • 阻塞问题: 如果一个用户级线程执行了阻塞式系统调用,整个进程都会被内核阻塞,即使其他线程是就绪的。
      • 多核利用问题: 内核只把CPU分配给进程,所以一个进程中的多个ULT不能在多核上并行执行。
    • 阻塞处理:
      • 使用非阻塞系统调用。
      • 使用 “Jacketing” / “Wrapper” 技术:库函数在调用可能阻塞的系统调用前检查,如果会阻塞,则不调用,而是切换到另一个用户线程。
  2. 核心级线程 (Kernel-Level Threads, KLT)

    • 实现: 线程的管理(创建、调度、同步)由操作系统内核完成。内核维护每个线程的TCB。
    • 优点:
      • 一个线程阻塞不影响进程内其他线程的执行。
      • 内核可以直接调度线程,可以在多核CPU上实现真正的并行。
    • 缺点:
      • 线程的创建、销毁、切换都需要进入内核态,开销比ULT大(但仍远小于进程切换)。
    • 例子: Windows 线程, Linux 的 NPTL (Native POSIX Thread Library,实际上是KLT)。
  3. 混合模型 (Hybrid Implementation)

    • 实现: 内核支持KLT,用户空间线程库将多个ULT映射到少量KLT上(M:N模型)。线程创建在用户态快,调度利用内核。
    • 例子: 早期的 Solaris。
    • 目标: 试图结合ULT的低开销和KLT的并发优势,但实现复杂,现在较少见,Linux和Windows都主要采用KLT模型。

4. 协程 (Coroutine)#

  1. 为什么引入协程?

    • 为了在单线程内实现更高效率的并发,尤其是针对 I/O 密集型任务和需要管理大量连接的场景。
    • 解决线程在高并发场景下的资源消耗(内存、内核调度开销)问题。
    • 用同步的方式编写异步代码,提高可读性。
  2. 协程是什么?

    • 一种 用户态的、协作式 的多任务实现。
    • 可以看作是比线程更轻量级的执行单元,由 程序员/运行时用户态 控制切换。
    • 协程可以在执行过程中的特定点 暂停 (yield),然后在未来从同一点 恢复 (resume)
  3. 协程怎么用?

    • 依赖于 编程语言或库 的支持。
    • 常见模式:
      • 定义协程函数(如 Python 的 async def)。
      • 在协程函数内部,遇到需要等待的操作(如异步I/O)时,使用特定关键字(如 await, yield) 主动让出控制权。
      • 一个事件循环 (Event Loop) 或调度器负责管理协程的暂停和恢复。
    • 例子: Python asyncio, Go goroutine, C++20 coroutine, Rust async/await
  4. 纤程 (Fiber)

    • Windows 操作系统提供的一种类似协程的机制,也是用户态调度的轻量级执行单元,一个线程内可以包含多个纤程。

5. 重点小结#

  • 进程 (Process):
    • 并发执行程序的实例,动态产生和消亡。
    • OS 资源分配 的独立单位,拥有独立地址空间。
    • 基本特征:并发性、动态性、独立性、制约性、异步性。
    • 进程映像 = 代码 + 数据 + 栈 + 堆 + PCB。
  • 线程 (Thread):
    • 进程内的一个执行流,CPU调度 的基本单位。
    • 拥有私有执行上下文(PC, Regs, Stack),共享进程资源(地址空间, 文件)。
    • 引入原因:应用并发需求、低开销、高性能(通信、并行)。
    • 实现方式:用户级 (ULT)、核心级 (KLT)、混合。各有优劣。
  • 协程 (Coroutine):
    • 用户态协作式多任务单元,比线程更轻量。
    • 切换开销极低,适合高并发I/O密集型任务。
    • 通过语言/库支持,用同步方式写异步代码。
  • 可再入程序 (Reentrant Program):
    • 可被多个进程同时调用的程序。
    • 特点:纯代码(执行中不修改自身),不使用静态/全局变量存储可变状态(或通过参数传入/线程本地存储)。
[中文] Operating Systems Notes: 03 - 进程线程模型
https://www.lyt0112.com//blog/operating_systems_note_03-zh
Author Yutong Liang
Published at March 27, 2025
Copyright CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨