{T}

进程和线程

进程(Process)是程序运行的实例,即软件的执行副本;线程(Thread)是轻量级进程(Light Weighted Process),是程序执行的基本单位。进程是资源分配的基本单位,线程是 CPU 调度的基本单位。

资源分配

进程需分配三类核心资源:

  • 计算资源(CPU):通过调度分配时间片段;
  • 内存资源:独立的虚拟地址空间;
  • 文件资源:打开的文件、I/O 设备等。

早期 OS 无线程,三类资源全部分配给进程,进程间通过管道等通信。但同一应用常有多个必须"同时进行"的行为(图形渲染、网络、响应用户输入),需将执行单位下沉——仅分配 CPU 资源的执行单元称为线程。

分时与调度

CPU 核心数少(几个到几十个)而进程/线程数多(几十到几百),因此通过分时轮流执行:每个执行单位获得一个时间片段,用尽后挂起、轮到下一个。现代 OS 直接调度线程。

分时调度

分时轮转

状态转换

进程/线程经历三个基本状态:

  • 就绪(Ready):创建后进入排队;
  • 运行(Running):轮到执行;
  • 阻塞(Block):等待磁盘/打印机等 I/O 时自行进入,待中断通知后回到就绪。

就绪-运行

阻塞

阻塞转就绪

状态总图

就绪态不能直接转阻塞(排队中无法触发 I/O 读取);阻塞态收到数据后需重新排队,不能直接回运行态。

进程与线程的表示

OS 在内存中维护进程表线程表(常驻内核)。

进程表

进程表包含:

  • 描述信息:PID、名称、所属用户;
  • 资源信息:虚拟内存映射、拥有的文件与 I/O 设备;
  • 内存布局:进程地址空间分段(代码段、数据段、堆、栈等)。

内存布局

线程表包含 ThreadID、执行状态、优先级、程序计数器(PC)与所有寄存器的值——因线程共用 CPU、频繁切换,需保存/恢复这些上下文。

用户级线程与内核级线程存在映射关系(如 n-m 多对多)。内核维护内核级线程表;用户级线程执行时才借用内核线程(线程池模式),执行完不销毁、等待下一个任务。

隔离方案

进程间分配彼此完全隔离的地址空间:即便访问相同虚拟地址,物理地址也不同(如 A、B 小区的 808 不是同一套房)。正常情况进程 A 无法访问进程 B 内存,除非利用漏洞或进程间通信手段。

地址空间隔离

同一进程的多个线程共享该进程的内存资源,仅被分配执行资源。

切换

现代 OS 只有线程切换。每次切换:

  1. OS 向 CPU 发中断信号,停止当前线程;
  2. CPU 立即停止,尚未保存自身状态;
  3. OS 接管,用底层程序(汇编)保存寄存器状态;
  4. 执行调度程序,选定下一线程;
  5. 恢复该线程寄存器状态后执行。

切换-中断

切换流程1

切换流程2

状态保存/恢复可用栈:中断时压栈关键数据(寄存器),恢复时出栈。

多核

多核系统中可并行执行与核数相同的线程。并发(concurrent)指一段时间内看上去同时执行(不要求多核);并行(parallel)指绝对同时执行(要求多核)。OS 的保存/恢复能力使线程可在多核间迁移。

多核并行

创建进程/线程的 API

  • fork:创建当前进程的克隆,新进程状态与原进程一致,但拥有独立地址空间。连续多个 fork 会指数级生成进程(n 次 fork 产生 (2^n) 个执行流)。
  • exec 类 API:启动全新程序。
  • 语言级线程 API:Java 的 Thread 类、Go 的 goroutine(轻量级线程)等。

fork

进程开销为何大于线程

创建进程自然创建主线程。进程需划分完整内存空间、做大量初始化(分段:栈、堆、代码段等);线程只需确定 PC 与寄存器值、分配一个执行栈,且同进程多线程可复用堆栈。因此创建进程更慢、内存开销更大。频繁处理请求等场景应优先用线程而非进程。

小结

进程承接存储资源、线程承接计算资源,进程包含线程并实现内存隔离。线程切换通过保存/恢复寄存器完成;多核下可并行。计算密集型任务进程开销不可忽略,应倾向多线程或 fork 复用。