Appearance
进程、线程与调度
进程和线程
- 进程(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 通过特权级别保护系统资源:
- 用户态:应用运行的受限模式,不能直接执行特权指令、操作页表或直接管理硬件。
- 内核态:操作系统内核运行的高特权模式,可管理硬件、内存、调度和文件系统。
状态切换常由以下事件触发:
- 系统调用:如
read、write、open,应用通过受控入口请求内核服务; - 异常:如缺页、除零、非法指令;
- 中断:如网卡、磁盘、时钟产生的外部事件;
- 调度:时钟中断或阻塞事件使内核选择其他可运行线程。
用户态/内核态切换不等同于进程或线程上下文切换;系统调用返回原线程时可能没有发生任务切换。
系统调用的进入机制: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 | 优先预计运行最短的作业 | 平均等待时间低;长任务可能饥饿,且难精确预测时长 |
| 最短剩余时间 SRTF | SJF 的抢占式版本 | 新短任务可抢占;长任务仍可能饥饿 |
| 优先级调度 | 按优先级选择 | 低优先级可能饥饿,可用 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;信号量是用于同步和资源计数的原子计数器与等待队列。普通非实时信号可能合并,不能用它承载可靠计数或消息队列。
共享内存快的原因是多个进程的虚拟地址都映射到同一组物理页,而不是“不需要内核”:创建、映射、取消映射和同步原语仍需要内核/硬件支持;数据读写路径则可直接访问这些页。