Skip to content

进程、线程与调度 ​

进程和线程 ​

  • 进程(process):运行中的程序实例,拥有独立虚拟地址空间、打开文件、句柄等资源,是资源隔离的重要边界。
  • 线程(thread):进程内的一条执行流,拥有自己的程序计数器、寄存器、栈和线程局部存储;同一进程的线程共享代码、堆、全局数据和多数进程资源。操作系统通常以线程作为实际调度实体。
对比进程线程
地址空间通常隔离同进程内共享
创建/切换资源和页表等状态更多,开销通常较大上下文较少,通常更轻量
通信管道、共享内存、Socket、消息队列等 IPC可直接共享内存,但需同步
隔离性一个进程崩溃通常不直接破坏其他进程一个线程越界/崩溃常导致整个进程受影响

为什么有进程之后还需要线程 ​

进程提供地址空间、文件描述符、权限和故障隔离,但若把“读取 → 解码/计算 → 输出”拆成多个进程并发执行,会有较高创建、地址空间切换和 IPC 成本。线程在同一进程内共享代码、堆、映射和打开文件,只为每条执行流保留独立寄存器上下文、用户栈和线程局部存储,因此适合把 I/O 等待、计算和 UI/网络响应等任务并发推进。

代价是共享内存也共享了风险:一个线程的越界访问、未同步写入或 std::terminate 通常会影响整个进程。线程不是“天然更快”,它用更低的通信成本交换了同步、生命周期和故障隔离的复杂度。

进程、线程与协程 ​

执行单元谁调度独立状态切换与边界
进程内核调度其中的线程独立虚拟地址空间和进程资源切换地址空间,隔离强、开销通常较大
内核线程内核调度器寄存器、PC、栈、TLS;共享进程资源可利用多核;阻塞系统调用通常只阻塞该线程
协程用户态运行时/执行器协程帧或用户态栈;共享所属线程地址空间切换很轻,但不会自动并行;阻塞调用会阻塞承载它的线程

协程不是操作系统调度实体,也不等同于线程。它必须配合非阻塞 I/O、异步 API 或运行时调度器才能在等待期间让出执行权;C++20 协程的语言机制见 模板、Concepts 与 C++20。

并发与并行 ​

  • 并发(concurrency):多个任务在同一时间段推进;单核可通过时间片交替实现。
  • 并行(parallelism):多个任务在同一时刻由多个硬件执行单元同时执行。

并发关注结构和任务协作;并行关注真实的同时执行与吞吐提升。多核程序可同时具备二者。

上下文切换:到底切换什么 ​

CPU 必须知道“下一条执行哪条指令、寄存器中间值是什么、当前栈在哪里”。发生时间片耗尽、阻塞、唤醒或更高优先级任务抢占时,内核大致执行:

text
当前线程运行
  → 保存当前线程的可见 CPU 状态
  → 调度器选择下一个可运行线程
  → 恢复目标线程状态
  → 从目标 PC 指向的位置继续执行

保存/恢复的核心包括程序计数器(PC/IP)、栈指针、通用寄存器、标志寄存器,以及按架构和使用情况处理的浮点/SIMD 寄存器;任务控制块(TCB,Linux 中对应 task_struct 及架构相关状态)和内核栈保存这些可恢复状态。它不是把“整个进程内存、全局变量、文件内容”复制一遍。

  • 同进程线程切换:通常复用同一页表和文件描述符表,主要切寄存器、栈、TLS 与调度状态;
  • 不同进程的线程切换:除上述状态外还要切换地址空间页表根(以及相关 TLB/地址空间标记处理),代价通常更高;
  • 额外代价:调度器执行、缓存/TLB 局部性被破坏、跨核迁移带来的 cache line 失效,往往比“保存几个寄存器”更值得关注。

因此线程数不是越多越好:过多线程会增加栈内存、调度和 cache 抖动;还会提高锁竞争、死锁与尾延迟风险。I/O 密集任务常用有界线程池或事件循环,CPU 密集任务的并行度一般先接近可用 CPU 核数,再用压测调整。

任务状态与转换 ​

教材常用的基本状态是:

  • 就绪(ready):拥有运行所需资源,只差 CPU;
  • 运行(running):当前正在某个 CPU 上执行;
  • 阻塞/等待(blocked/waiting):正在等待 I/O、锁、条件变量、定时器或子进程,不能被调度运行;
  • 终止(terminated):任务已退出,内核仍可能短暂保留退出状态供父进程 wait 回收。

“阻塞 → 就绪”不代表立即运行,仍需等待调度器分配 CPU。操作系统还可能区分可中止/不可中止睡眠、停止(stopped)和换出(suspended)等状态;面试中先讲清 ready、running、blocked 的区别即可。

用户态与内核态 ​

CPU 通过特权级别保护系统资源:

  • 用户态:应用运行的受限模式,不能直接执行特权指令、操作页表或直接管理硬件。
  • 内核态:操作系统内核运行的高特权模式,可管理硬件、内存、调度和文件系统。

状态切换常由以下事件触发:

  1. 系统调用:如 read、write、open,应用通过受控入口请求内核服务;
  2. 异常:如缺页、除零、非法指令;
  3. 中断:如网卡、磁盘、时钟产生的外部事件;
  4. 调度:时钟中断或阻塞事件使内核选择其他可运行线程。

用户态/内核态切换不等同于进程或线程上下文切换;系统调用返回原线程时可能没有发生任务切换。

系统调用的进入机制:int 0x80 与 syscall ​

应用请求内核服务时要从用户态切入内核态,x86 上有两条典型路径:

路径机制特点
int 0x80(软件中断/中断门)通过中断描述符表(IDT)转到内核入口,特权级变化时处理器压栈保存 SS/ESP/EFLAGS/CS/EIP 等上下文并切换到内核栈通用但重:需要查 IDT 并走完整的中断门进入/返回路径;它是软件中断,不经过外部 PIC/APIC 中断控制器
syscall/sysret(快速系统调用)CPU 专用指令,直接读取 MSR(如 IA32_LSTAR)跳转内核入口,少量寄存器由指令自动保存轻量、延迟低,适合高频系统调用

为什么新架构(x86-64)多用 syscall:省去中断门全套压栈/弹栈与 IDT 查找,指令路径短、硬件直接支持,现代 CPU 对 syscall 有专门优化;高频操作(read/write/epoll_wait)下延迟差异明显。32 位时代还有 sysenter/sysexit(思路类似,但有边界与权限问题,需 VDSO 辅助)。另外注意 vDSO:gettimeofday 等无状态系统调用可在用户态直接完成,根本不进内核。

延伸:对运行中进程做系统调用级诊断(strace)的原理与生产环境风险,见 诊断与排障。

常见调度算法 ​

算法特点问题/适用点
先来先服务 FCFS按到达顺序执行简单,但长任务会造成护航效应
最短作业优先 SJF优先预计运行最短的作业平均等待时间低;长任务可能饥饿,且难精确预测时长
最短剩余时间 SRTFSJF 的抢占式版本新短任务可抢占;长任务仍可能饥饿
优先级调度按优先级选择低优先级可能饥饿,可用 aging 提升等待者优先级
时间片轮转 RR轮流执行固定时间片适合分时交互;时间片过大退化 FCFS,过小切换开销高
多级反馈队列 MLFQ多个优先级队列,按行为动态迁移兼顾交互和吞吐,是通用系统常见思路

CFS 与容器 CPU 配额 ​

Linux 默认调度器 CFS(完全公平调度器)按 vruntime 维护全局公平:每个可运行任务按权重获得比例化的 CPU 时间,睡眠任务 vruntime 不增长、醒来后获得补偿。

容器 CPU 配额 = CFS 带宽控制(cgroup cpu 控制器):cpu.cfs_quota_us / cpu.cfs_period_us 规定一个周期内任务组最多使用的 CPU 时间(如 quota=400ms、period=100ms → 最多 4 核);超出配额的 cgroup 任务被 throttle(限流),即使系统空闲也必须等待下一周期。

对延迟敏感型服务(如游戏服务器)的影响:

  • 限流导致瞬时 CPU 饥饿:逻辑帧、心跳、网络轮询出现延迟尖刺,Tick 节奏抖动;
  • CFS 的 vruntime 公平是长期吞吐公平,对延迟不敏感:配额耗尽瞬间不会因“全局空闲”而放行;
  • 配额与核数不匹配时(如配额 2 核但线程 8 个)线程排队、上下文切换增加;
  • 对策:延迟敏感服务绑核 + 预留独占核(配额设整核、isolcpus),避免配额过小,监控 cpu.stat 的 nr_throttled 限流次数。

进程间通信(IPC) ​

方式特征
匿名管道内核缓冲区字节流,常用于有亲缘关系的进程,通常单向
命名管道 FIFO有路径名,可用于无亲缘进程,仍是字节流
消息队列以消息为单位传递,可带类型/优先级
共享内存多进程映射同一物理页,数据通路最快,但必须额外同步
信号异步通知某事件,携带信息有限,不适合传大量数据
Socket可跨主机,也可本机 UNIX domain socket;接口通用
信号量/进程共享锁主要用于协调对共享资源的访问,而非承载大量数据

条件变量、互斥锁常用于线程同步;在支持进程共享属性和共享内存时,也可被用于进程间同步。

IPC 如何选择 ​

需求优先选择原因与边界
父子进程串行传输字节流匿名管道内核维护的 FIFO 字节流;双向通信需要两条管道,读写端必须正确关闭
无亲缘本机进程通过路径通信命名管道 FIFO有文件系统路径;仍是字节流,需自行定义消息边界
本机高吞吐共享大量数据共享内存 + 同步原语多个页表映射到相同物理页,避免数据通路复制;必须自行处理互斥、可见性和生命周期
结构化、小到中等消息消息队列 / Unix domain socket消息队列保留消息边界;Unix socket 通用且可传递凭据、文件描述符等
跨主机通信TCP / UDP / QUIC socket协议、网络失败、序列化和安全边界都需显式处理
通知进程发生异步事件信号 / eventfd / 管道信号不适合传大量业务数据;信号处理函数限制严格

信号(signal)和信号量(semaphore)不是一回事:信号是内核递送的异步事件通知,例如 SIGTERM、SIGCHLD;信号量是用于同步和资源计数的原子计数器与等待队列。普通非实时信号可能合并,不能用它承载可靠计数或消息队列。

共享内存快的原因是多个进程的虚拟地址都映射到同一组物理页,而不是“不需要内核”:创建、映射、取消映射和同步原语仍需要内核/硬件支持;数据读写路径则可直接访问这些页。

使用 Markdown 与 VitePress 构建