文章

Linux 内核学习笔记:进程与线程

Linux 内核学习笔记:进程与线程

这一篇记录进程与线程的几个基础问题:进程基础、线程、调度入口、进程间通信、多线程同步、死锁、锁类型。这一块内容很宽,本文先整理主线;Linux 细节集中落到 task_struct、地址空间、文件表、调度状态、IPC 对象、futex 和等待队列。

问题索引

主题本篇记录
进程程序运行实例、地址空间、资源边界
进程状态创建、就绪、运行、阻塞、结束、挂起;Linux R/S/D/T/Z
PCB教材 PCB 到 Linux task_struct
进程控制创建、终止、阻塞、唤醒;fork()、clone()、execve()、exit()、wait()
上下文切换CPU 上下文、地址空间切换、内核栈、调度器
线程共享进程资源的执行流;Linux 里对应 task 和线程组
调度入口调度时机、调度原则、调度算法放到下一章细讲
IPCpipe、message queue、shared memory、semaphore、signal、socket
多线程同步竞争、互斥、同步、锁、信号量、futex
死锁和锁类型四个必要条件、排查入口、互斥锁/自旋锁/读写锁/乐观锁

1. 进程:程序的一次运行实例

进程是程序运行时形成的执行实体和资源容器。可执行文件在磁盘上只是静态文件;运行后,内核为它建立地址空间、打开文件表、凭据、信号状态、调度状态等运行时对象。

名词记录
program静态程序文件或代码集合
process程序的一次运行实例,拥有地址空间和一组资源
address space进程可见的虚拟地址范围
resource boundary内存、文件描述符、权限、namespace、cgroup 等资源归属边界
execution contextCPU 寄存器、栈、程序计数器等执行状态

OSTEP 的进程抽象可以压缩成两点:进程让程序看起来独占 CPU,地址空间让程序看起来独占一段连续内存。OS 通过调度和地址翻译实现这两个抽象。

Linux 落点:

Linux 对象作用
task_structLinux 可调度实体和资源引用集合
mm_struct用户态地址空间
files_struct文件描述符表
fs_struct当前目录、根目录、umask 等文件系统上下文
creduid、gid、capability 等权限信息
nsproxynamespace 引用

进程主题里抓 task_struct 是为了定位创建、运行、阻塞、唤醒、退出这些动作。内存管理、文件系统、网络栈有各自的核心对象。

2. 进程状态:教材模型和 Linux 状态

教材里的基本状态是运行、就绪、阻塞。扩展后还会加入创建、结束、挂起。它们描述的是进程和 CPU、事件、内存驻留之间的关系。

教材状态含义Linux 观察
running正在 CPU 上执行R 中正在运行的 task
ready可运行,等待 CPUR 中在 runqueue 等待的 task
blocked等待 I/O、锁、事件S 或 D
stopped被作业控制或调试暂停T、t
zombie已退出,等待父进程回收Z

Linux 的 ps STAT 字段是观察入口,但它是用户态展示,不能和教材状态一一硬套。

Linux 状态常见含义
Rrunnable 或 running
Sinterruptible sleep,可被信号唤醒
Duninterruptible sleep,常见于等待内核 I/O 路径
T / tstopped 或 tracing stop
Zzombie

观察入口:

命令 / 路径记录
ps -o pid,ppid,stat,wchan,comm -p <pid>看状态和等待点
/proc/<pid>/status看 State、Tgid、Pid、Threads
/proc/<pid>/wchan看睡眠时等待的内核符号
perf sched latency看 runnable 后等待 CPU 的延迟

3. PCB 到 task_struct

教材用 PCB 描述进程控制块。PCB 保存进程标识、状态、寄存器上下文、调度信息、内存信息、打开文件、父子关系等。Linux 中主要落到 task_struct,再由它引用其他子系统对象。

PCB 信息Linux 落点
进程标识pid、tgid、thread group
进程状态task_struct 的 state / exit_state
CPU 上下文架构相关 thread context、内核栈
调度信息priority、policy、sched entity、runqueue
内存信息mm_struct、页表、VMA
文件信息files_struct、fdtable、struct file
父子关系parent、children、sibling
信号signal、sighand、blocked、pending

task_struct 适合按“执行状态 + 资源引用 + 关系 + 调度字段”四组记。源码入口是 include/linux/sched.h,但这个结构体很大,阅读时需要带着路径问题看字段。

4. 进程控制:创建、替换、退出、回收

进程控制在教材里通常包含创建、终止、阻塞、唤醒。Linux 里常见路径可以按 syscall 记。

动作用户态接口Linux 主线
创建进程fork()创建新 task,父子资源隔离较强,地址空间使用 COW
创建线程 / 特定共享关系clone() / clone3()通过 flags 控制共享 mm_struct、files_struct 等
替换程序映像execve()当前 task 装载新 ELF,重建用户地址空间
退出exit() / exit_group()释放资源,留下退出状态
回收wait() / waitpid() / wait4()父进程读取退出状态,释放 zombie 记录

fork() 后父子进程逻辑上拥有独立地址空间,物理页常通过 COW 延迟复制。打开文件描述符会继承;父子 fd 可能指向同一个 struct file,因此共享文件偏移。

execve() 改的是当前 task 的用户态程序映像。pid 继续沿用,地址空间、用户栈、代码段、数据段会换成新程序;带 FD_CLOEXEC 的 fd 在 exec 时关闭。

exit() 后出现 zombie,是因为父进程还没有读取退出状态。zombie 的主要运行资源已经释放,保留 pid、退出码、统计信息等回收所需内容。

源码入口:

路径内容
kernel/fork.cfork/clone 和 task 创建
fs/exec.cexecve 和 ELF 装载
kernel/exit.cexit、wait、reparent
kernel/signal.csignal 投递和处理

5. 上下文切换:CPU 状态和地址空间

上下文切换保存当前执行实体的必要状态,再恢复另一个执行实体的状态。CPU 上下文至少包括通用寄存器、栈指针、程序计数器、状态寄存器等。进程切换还可能涉及地址空间和 TLB。

类型切换内容
进程上下文切换CPU 寄存器、内核栈、调度状态、地址空间、TLB 影响
同进程线程切换CPU 寄存器、栈、线程私有状态;通常共享地址空间
中断上下文切换当前执行流被中断,CPU 进入中断处理路径

常见触发点:

场景记录
时间片或调度周期当前 task 让出 CPU
主动睡眠sleep()、等待锁、等待 I/O、等待事件
唤醒抢占更高优先级或更合适的 task 被唤醒
系统调用阻塞进入内核后等待资源
中断CPU 转入中断处理,可能引发后续调度

Linux 入口:schedule()、context_switch()、switch_to()、sched_switch tracepoint。观察入口:pidstat -w 1、vmstat 1、perf sched record、perf sched latency。

6. 线程:共享资源的执行流

线程是进程内的一条执行流。多个线程共享同一进程的地址空间、代码段、数据段、打开文件等资源;每个线程有自己的寄存器、栈、线程局部状态和调度状态。

对象同进程线程之间
地址空间共享
全局变量 / 堆共享
打开文件表通常共享
当前工作目录等 fs 上下文通常共享
寄存器各自独立
用户栈各自独立
signal mask每线程独立

Linux 的线程是 task。一个多线程进程对应多个 task_struct,这些 task 共享 mm_struct 等资源,并处在同一个 thread group 中。

名词记录
pid单个 task 的标识
tgidthread group id,通常是用户态看到的进程 id
thread group同一进程内的一组 task
LWP轻量级进程,Linux 线程模型常用说法
CLONE_THREAD创建同线程组 task 的关键 flag

观察入口:

命令 / 路径记录
ps -eLf展开线程
top -H -p <pid>看进程内线程
/proc/<pid>/task线程目录
/proc/<pid>/statusThreads、Pid、Tgid

7. 调度入口:第四章只记接口

调度时机、调度原则和调度算法放到下一篇“调度算法”处理;本文只记进程/线程和调度器的接口。

问题记录
谁被调度Linux 调度 task
在哪里等待 CPU每 CPU runqueue
什么状态能被调度runnable task
阻塞后怎么回来等待事件完成后 wakeup,重新进入 runnable
调度策略在哪里体现scheduling class、priority、nice、policy

进程/线程学习里需要记住的边界:阻塞等待事件和可运行但等待 CPU 是两种不同状态。前者在等 I/O、锁、条件;后者已经能运行,只是在 runqueue 上排队。

8. 进程间通信:六类入口

进程地址空间隔离后,需要 IPC 交换数据或事件。这里按管道、消息队列、共享内存、信号量、信号、Socket 记录。

IPC数据形态Linux 落点
pipe字节流亲缘进程常见;匿名管道通过 fd 传递
FIFO字节流有路径名的管道
message queue离散消息System V / POSIX 消息队列
shared memory共享内存区域数据不需要反复拷贝,必须配合同步
semaphore计数和同步System V / POSIX semaphore;线程内也有信号量
signal异步事件通知kill()、异常、终端事件等
socket字节流或数据报本机进程和跨机器通信都可用

Linux 里所有这些机制都能和“等待/唤醒”连起来。读空管道会睡眠,socket receive queue 没数据会睡眠,futex 等待锁会睡眠;事件到来后,内核唤醒等待队列上的 task。

9. 多线程同步:竞争、互斥、同步

多个线程共享地址空间,读写共享数据时会出现竞争。同步笔记先区分三个词。

名词记录
race condition执行结果依赖线程交错顺序
mutual exclusion同一时刻只允许一个线程进入临界区
synchronization按条件或顺序协调多个线程
critical section访问共享资源的一段代码
atomic operation不会被观察到中间状态的操作

Linux 落点:

用户态机制内核落点
pthread mutex无竞争时用户态 fast path;竞争时可进入 futex
condition variable配合 mutex;等待条件变化时睡眠和唤醒
semaphore计数型同步
rwlock读多写少场景
futex用户态地址上的 fast userspace mutex,竞争时由内核挂等待队列

futex 是理解用户态锁和内核调度关系的关键点:锁状态放在用户态内存;抢锁失败时,线程通过 futex syscall 进入内核睡眠;解锁线程在需要时唤醒等待者。

10. 死锁:条件、排查、避免

死锁由多个执行流互相等待资源形成。经典四个必要条件需要直接记。

条件含义
互斥资源同一时刻只能被一个执行流持有
持有并等待已持有资源时继续等待其他资源
不可剥夺资源不能被外部强制抢走
环路等待等待关系形成环

避免死锁常用方法是破坏环路等待:给锁规定全局顺序,所有线程按同一顺序加锁。还可以通过 try-lock、超时、锁粒度调整、减少持锁期间阻塞操作等方式降低风险。

排查入口:

工具记录
pstack <pid>快速看多线程调用栈
gdb -p <pid>查看线程、栈、锁对象
info threadsgdb 中列出线程
thread apply all bt打印所有线程栈
perf lock分析内核锁事件,依配置可用
/proc/<pid>/stack看内核栈,权限和内核配置相关

用户态 pthread 死锁常用栈和锁 owner 定位;内核锁问题还需要结合 lockdep、ftrace、perf lock、hung task 日志。

11. 锁类型:成本和等待方式

互斥锁、自旋锁、读写锁、乐观锁、悲观锁按等待方式和适用条件整理。

锁加锁失败后的行为适合场景
mutex睡眠,等待唤醒临界区可能较长,可以睡眠
spinlock忙等,占 CPU临界区很短,不能睡眠的内核路径
rwlock / rwsem区分读写读多写少
optimistic lock先操作或读取,再验证版本冲突概率低,重试成本可接受
pessimistic lock访问前先排他保护冲突概率高或数据一致性要求强

Linux 内核里要特别区分能否睡眠。进程上下文通常可以睡眠;中断上下文不能随便睡眠。spinlock 保护的临界区里也不能调用可能睡眠的函数。

压缩表

概念基础概念 / 八股OSTEP 模型Linux 落点
进程程序运行实例CPU 和内存虚拟化的执行实体task_struct、mm_struct、files_struct
线程进程内执行流共享地址空间的控制流task、thread group、pid/tgid
PCB进程控制块保存状态和资源引用task_struct
上下文切换保存/恢复运行状态受限直接执行和调度schedule()、context_switch()
IPC进程间通信方式隔离后的通信机制pipe、shm、signal、socket、wait queue
同步互斥和协作并发控制mutex、semaphore、futex、RCU
死锁四个必要条件资源等待图lock order、pstack、gdb、lockdep

检查点与参考回答

问题参考回答
进程是什么?进程是程序的一次运行实例,也是地址空间、文件描述符、权限、信号等资源的容器。Linux 中通常由一个或多个 task 组成。
线程是什么?线程是进程内的一条执行流。同一进程内线程共享地址空间、全局数据和打开文件等资源,各自拥有寄存器、栈和调度状态。
进程和线程的区别是什么?进程更强调资源边界,线程更强调执行流。同进程线程共享资源,所以创建和切换成本通常更低,但共享数据需要同步,一个线程的严重错误可能影响整个进程。
PCB 在 Linux 中对应什么?教材 PCB 在 Linux 中主要对应 task_struct,同时还要通过它引用 mm_struct、files_struct、signal、cred、namespace、cgroup 等对象。
fork() 和 execve() 分别做什么?fork() 创建新进程,子进程继承父进程资源并通过 COW 延迟复制地址空间;execve() 在当前 task 上装载新程序映像,重建用户地址空间。
上下文切换切什么?至少切 CPU 寄存器、栈指针、程序计数器等执行状态。进程切换还可能切地址空间和影响 TLB;同进程线程切换通常共享地址空间。
为什么同进程线程切换通常比进程切换便宜?同进程线程共享 mm_struct 和页表,切换时通常不需要切换地址空间,TLB 影响较小,主要切换线程私有寄存器、栈和调度状态。
IPC 有哪些常见方式?管道、FIFO、消息队列、共享内存、信号量、信号、Socket。共享内存吞吐高,但需要额外同步。Socket 既能本机通信,也能跨机器通信。
futex 解决什么问题?futex 让用户态锁在无竞争时不进入内核;竞争时线程通过 futex syscall 睡眠,解锁方按需唤醒等待者。
死锁的四个必要条件是什么?互斥、持有并等待、不可剥夺、环路等待。破坏其中任一条件即可避免死锁,工程中常用全局锁顺序破坏环路等待。
互斥锁和自旋锁怎么选?临界区可能较长或可以睡眠时用 mutex;临界区极短、不能睡眠的内核路径用 spinlock。自旋锁忙等会消耗 CPU。

本篇参考

  • Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau, Operating Systems: Three Easy Pieces
  • Michael Kerrisk, The Linux Programming Interface
  • W. Richard Stevens, Stephen A. Rago, Advanced Programming in the UNIX Environment
  • Robert Love, Linux Kernel Development
  • Linux Kernel Documentation: Scheduler, locking, futex, core API
本文由作者按照 CC BY 4.0 进行授权