进程(Process)和线程(Thread)是操作系统中两个核心概念。理解它们的区别、特点和使用场景对于系统编程和性能优化非常重要。
进程
什么是进程
进程是程序的一次执行过程,是操作系统进行资源分配和调度的基本单位。每个进程都有独立的地址空间和系统资源。
进程的定义
从不同角度看进程:
- 程序执行角度:进程是正在执行的程序
- 资源分配角度:进程是资源分配的基本单位
- 系统调度角度:进程是 CPU 调度的实体
进程的特点
graph TB
A[进程] --> B[独立地址空间]
A --> C[独立资源]
A --> D[进程控制块 PCB]
B --> B1[代码段]
B --> B2[数据段]
B --> B3[堆栈段]
C --> C1[文件描述符]
C --> C2[信号处理]
C --> C3[环境变量]
style A fill:#ffcccc
style B fill:#ccffcc
style C fill:#ccccff
进程的结构
进程的组成部分
一个进程通常包含以下部分:
| 组成部分 | 说明 |
|---|---|
| 代码段(Text) | 程序的机器指令 |
| 数据段(Data) | 全局变量和静态变量 |
| 堆(Heap) | 动态分配的内存 |
| 栈(Stack) | 局部变量、函数参数、返回地址 |
| 进程控制块(PCB) | 进程的状态信息 |
进程的内存布局
graph TB
A[高地址] --> B[栈 Stack
向下增长]
B --> C[未使用空间]
C --> D[堆 Heap
向上增长]
D --> E[数据段 Data
BSS]
E --> F[代码段 Text]
F --> G[低地址]
style B fill:#ffcccc
style D fill:#ccffcc
style E fill:#ffffcc
style F fill:#ccccff
进程控制块(PCB)
PCB 是操作系统管理进程的数据结构,包含:
1 | // PCB 的主要字段(简化) |
进程的状态
进程状态转换
stateDiagram-v2
[*] --> 创建: fork()
创建 --> 就绪: 分配资源完成
就绪 --> 运行: 被调度
运行 --> 就绪: 时间片用完
运行 --> 阻塞: 等待 I/O 或事件
阻塞 --> 就绪: I/O 完成或事件发生
运行 --> 终止: exit()
终止 --> [*]
note right of 运行
正在 CPU 上执行
end note
note right of 阻塞
等待资源或事件
不占用 CPU
end note
进程状态说明
| 状态 | 说明 | 特点 |
|---|---|---|
| 创建(New) | 进程刚被创建 | 分配资源,初始化 PCB |
| 就绪(Ready) | 进程已准备好运行 | 等待 CPU 调度 |
| 运行(Running) | 进程正在 CPU 上执行 | 占用 CPU |
| 阻塞(Blocked) | 进程等待某个事件 | 不占用 CPU |
| 终止(Terminated) | 进程执行完毕 | 释放资源 |
进程的创建
fork() 系统调用
fork() 是 Linux 中创建进程的主要方式:
1 |
|
fork() 的特点:
- 创建子进程,是父进程的副本
- 子进程获得父进程的代码、数据、堆栈的副本
- 返回两次:父进程返回子进程 PID,子进程返回 0
- 失败返回 -1
fork() 示例
1 |
|
输出示例:
1 | Before fork, PID: 1234 |
fork() 的执行流程
sequenceDiagram
participant P as 父进程
participant K as 内核
participant C as 子进程
P->>K: fork() 系统调用
K->>K: 创建子进程 PCB
K->>K: 复制父进程地址空间
K->>K: 复制文件描述符表
K->>C: 创建子进程
K->>P: 返回子进程 PID
K->>C: 返回 0
Note over P: 继续执行
Note over C: 从 fork() 返回后继续执行
fork() 的写时复制(COW)
现代 Linux 使用写时复制(Copy-On-Write)优化:
flowchart TD
A[fork 创建子进程] --> B[共享父进程的物理页面]
B --> C{子进程写入页面?}
C -->|否| D[继续共享]
C -->|是| E[复制页面]
E --> F[子进程使用新页面]
style B fill:#ccffcc
style E fill:#ffcccc
COW 的优势:
- 减少内存复制
- 提高 fork() 性能
- 节省内存空间
进程的终止
正常终止
1 | // 方式1:从 main 返回 |
异常终止
1 | // 方式1:调用 abort() |
exit() 与 _exit() 的区别
| 函数 | 说明 | 执行清理 |
|---|---|---|
| exit() | 标准库函数 | 是(刷新缓冲区、调用退出处理函数) |
| _exit() | 系统调用 | 否(立即退出) |
进程的等待
wait() 和 waitpid()
父进程可以等待子进程结束:
1 |
|
示例:
1 |
|
进程间通信(IPC)
进程间通信的方式包括:
- 管道(Pipe)
- 命名管道(FIFO)
- 消息队列(Message Queue)
- 共享内存(Shared Memory)
- 信号量(Semaphore)
- 信号(Signal)
- Socket
详细说明请参考《进程间通信》文档。
进程分类
在 Linux 操作系统中,进程可以按照其生命周期和角色分为不同类型,常见的有以下几类:
用户进程
用户进程是直接或间接由用户启动的进程,包括通过终端、图形界面或脚本启动的各种应用程序(如 bash shell、vim、firefox、gcc 等)。它们的资源与权限受当前用户身份和系统策略限制。
特点:
- 由用户发起(登录 shell、双击程序、脚本等)
- 提供具体的应用功能
- 拥有与所属用户相同的权限
- 终止不影响系统核心服务
内核进程
内核进程(kernel thread)由操作系统内核管理,不属于任何一个用户空间程序。这些进程运行在内核态,通常用于内核任务调度、内存管理、IO 处理等。它们没有用户空间地址映射,不与用户进程直接交互,也没有控制终端。
典型内核进程:
- kswapd(内存回收)
- ksoftirqd(软中断处理)
- kthreadd(内核线程调度器)
- kworker、kblockd 等异步任务处理
特点:
- 仅运行在内核态,无用户空间
- 不受用户直接控制或终止
- 负责系统底层功能和硬件管理
孤儿进程
孤儿进程指的是其父进程退出(终止)后仍然运行的进程。孤儿进程不会被系统遗弃,而是由 init 进程(PID 1,现代系统为 systemd)接管,成为其子进程。这样做的目的是保证内核能够正常跟踪和管理这些孤儿进程资源。
- 产生原因:父进程先于子进程终止。
- 处理方式:init 进程负责调用
wait()或waitpid(),释放其资源。
僵尸进程
僵尸进程是指已经终止(退出)的子进程,但其父进程尚未调用 wait() 或 waitpid() 回收其资源(如退出状态等),进而在进程表中留下一个占位符。僵尸进程本身不占用 CPU 和内存,但会占用有限的进程号(PID),积累过多会导致系统无法创建新进程。
- 产生原因:父进程没有及时回收子进程资源。
- 解决方法:父进程应及时调用
wait()或waitpid();如果父进程退出,init 进程会自动清理僵尸进程。
僵尸进程的状态通常会显示为 “Z” (zombie) 在 ps 或 top 命令中。
守护进程
守护进程(Daemon Process)是在后台运行的特殊进程,脱离了控制终端,通常用于执行系统服务(如 crond、sshd、nginx 等)。
- 典型特点:
- 没有控制终端(与任何终端无关)
- 以 root 或特定系统用户身份运行
- 生命周期通常较长,随系统启动而启动
- 实现方法:
- fork 两次,父进程退出,孙进程成为真正的守护进程
- 关闭文件描述符,重定向输入输出
- 脱离控制终端,设置为新会话首进程(setsid)
守护进程是保证系统和服务长期稳定运行的重要角色。
线程
什么是线程
线程是进程内的执行流,是 CPU 调度的基本单位。多个线程共享进程的地址空间和资源。
线程的定义
从不同角度看线程:
- 执行角度:线程是进程内的执行流
- 资源角度:线程共享进程的资源
- 调度角度:线程是 CPU 调度的基本单位
线程的特点
graph TB
A[进程] --> B[线程1]
A --> C[线程2]
A --> D[线程3]
A --> E[共享资源]
E --> E1[地址空间]
E --> E2[文件描述符]
E --> E3[信号处理]
B --> F1[独立栈]
C --> F2[独立栈]
D --> F3[独立栈]
B --> G1[独立寄存器]
C --> G2[独立寄存器]
D --> G3[独立寄存器]
style A fill:#ffcccc
style E fill:#ccffcc
style F1 fill:#ccccff
style F2 fill:#ccccff
style F3 fill:#ccccff
线程的结构
线程的组成部分
线程共享进程的资源,但拥有独立的部分:
| 共享部分 | 独立部分 |
|---|---|
| 代码段 | 栈(Stack) |
| 数据段 | 寄存器 |
| 堆(Heap) | 程序计数器(PC) |
| 文件描述符 | 线程局部存储(TLS) |
| 信号处理 | 线程 ID |
线程的内存布局
graph TB
A[进程地址空间] --> B[代码段
共享]
A --> C[数据段
共享]
A --> D[堆
共享]
A --> E[线程1栈
独立]
A --> F[线程2栈
独立]
A --> G[线程3栈
独立]
style B fill:#ccffcc
style C fill:#ccffcc
style D fill:#ccffcc
style E fill:#ffcccc
style F fill:#ffcccc
style G fill:#ffcccc
线程的状态
线程状态转换
stateDiagram-v2
[*] --> 就绪: pthread_create()
就绪 --> 运行: 被调度
运行 --> 就绪: 时间片用完
运行 --> 阻塞: 等待锁/条件变量
阻塞 --> 就绪: 条件满足
运行 --> 终止: pthread_exit()
终止 --> [*]
note right of 运行
正在 CPU 上执行
end note
note right of 阻塞
等待同步原语
不占用 CPU
end note
线程的创建
pthread_create()
POSIX 线程(pthread)是 Linux 中创建线程的标准方式:
1 |
|
参数说明:
thread:线程 ID 的指针attr:线程属性(NULL 使用默认属性)start_routine:线程函数arg:传递给线程函数的参数
pthread_create() 示例
1 |
|
编译和运行
1 | # 编译(需要链接 pthread 库) |
线程的分类
线程可以根据实现方式和管理层次进行分类,主要包括用户级线程和内核级线程,以及不同的线程实现模型。
用户级线程(User-level Thread, ULT)
用户级线程是由用户空间的线程库(如 pthread)管理的线程,内核不知道这些线程的存在。
graph TB
A[用户空间] --> B[线程库
pthread]
B --> C[用户线程1]
B --> D[用户线程2]
B --> E[用户线程3]
F[内核空间] --> G[内核线程
LWP]
C -.->|映射到| G
D -.->|映射到| G
E -.->|映射到| G
style A fill:#ccffcc
style F fill:#ffcccc
style B fill:#ffffcc
style G fill:#ccccff
特点:
- 管理位置:用户空间线程库管理
- 内核感知:内核不知道用户线程的存在
- 调度:由线程库在用户空间调度
- 阻塞影响:一个线程阻塞会导致整个进程阻塞
- 切换开销:小(用户空间切换)
- 实现:完全在用户空间实现
优点:
- 线程切换不需要系统调用,开销小
- 线程库可以自定义调度策略
- 不占用内核资源
缺点:
- 一个线程阻塞会导致整个进程阻塞
- 无法利用多核 CPU(内核只看到一个进程)
- 内核无法调度用户线程
内核级线程(Kernel-level Thread, KLT)
内核级线程是由内核直接管理的线程,每个线程在内核中都有对应的数据结构。
graph TB
A[用户空间] --> B[线程1]
A --> C[线程2]
A --> D[线程3]
E[内核空间] --> F[内核线程1]
E --> G[内核线程2]
E --> H[内核线程3]
B -->|一对一映射| F
C -->|一对一映射| G
D -->|一对一映射| H
style A fill:#ccffcc
style E fill:#ffcccc
特点:
- 管理位置:内核直接管理
- 内核感知:内核知道每个线程
- 调度:由内核调度器调度
- 阻塞影响:一个线程阻塞不影响其他线程
- 切换开销:较大(需要系统调用)
- 实现:内核实现
优点:
- 一个线程阻塞不影响其他线程
- 可以利用多核 CPU
- 内核可以更好地调度和负载均衡
缺点:
- 线程切换需要系统调用,开销较大
- 占用更多内核资源
- 调度策略由内核决定
线程实现模型
根据用户线程和内核线程的映射关系,可以分为三种模型:
1. 一对一模型(One-to-One Model)
每个用户线程映射到一个内核线程。
graph LR
A[用户线程1] --> B[内核线程1]
C[用户线程2] --> D[内核线程2]
E[用户线程3] --> F[内核线程3]
style A fill:#ccffcc
style C fill:#ccffcc
style E fill:#ccffcc
style B fill:#ffcccc
style D fill:#ffcccc
style F fill:#ffcccc
特点:
- 每个用户线程对应一个内核线程
- 真正的并行执行
- 线程切换开销较大
- 受内核线程数量限制
实现:
- Linux 的 NPTL(Native POSIX Thread Library)
- Windows 线程
示例:
1 | // Linux NPTL 实现 |
2. 多对一模型(Many-to-One Model)
多个用户线程映射到一个内核线程。
graph LR
A[用户线程1] --> D[内核线程]
B[用户线程2] --> D
C[用户线程3] --> D
style A fill:#ccffcc
style B fill:#ccffcc
style C fill:#ccffcc
style D fill:#ffcccc
特点:
- 多个用户线程共享一个内核线程
- 线程切换在用户空间完成,开销小
- 一个线程阻塞会导致整个进程阻塞
- 无法利用多核 CPU
实现:
- GNU Portable Threads(已废弃)
- 某些旧的操作系统
缺点:
- 无法真正并行执行
- 一个线程阻塞影响所有线程
- 现代系统很少使用
3. 多对多模型(Many-to-Many Model)
多个用户线程映射到多个内核线程(数量可以不同)。
graph TB
A[用户线程1] --> E[内核线程1]
B[用户线程2] --> E
C[用户线程3] --> F[内核线程2]
D[用户线程4] --> F
style A fill:#ccffcc
style B fill:#ccffcc
style C fill:#ccffcc
style D fill:#ccffcc
style E fill:#ffcccc
style F fill:#ffcccc
特点:
- 灵活的用户线程和内核线程映射
- 可以动态调整内核线程数量
- 结合了一对一和多对一的优点
- 实现复杂
实现:
- Solaris 的线程实现
- 某些研究性操作系统
Linux 的线程实现
NPTL(Native POSIX Thread Library)
Linux 使用 NPTL 实现 POSIX 线程,采用一对一模型:
graph TB
A[pthread_create] --> B[clone 系统调用]
B --> C[创建轻量级进程 LWP]
C --> D[内核线程]
E[用户线程] -->|一对一映射| D
style A fill:#ccffcc
style B fill:#ffffcc
style C fill:#ccccff
style D fill:#ffcccc
NPTL 的特点:
- 一对一模型:每个 pthread 对应一个内核线程(LWP)
- 轻量级进程(LWP):Linux 将线程实现为轻量级进程
- 共享资源:多个 LWP 共享地址空间
- 独立调度:每个 LWP 可以独立调度
轻量级进程(LWP)
Linux 中的线程实际上是通过轻量级进程(Lightweight Process, LWP)实现的:
graph TB
A[进程] --> B[LWP 1
主线程]
A --> C[LWP 2
线程1]
A --> D[LWP 3
线程2]
A --> E[共享资源]
E --> E1[地址空间]
E --> E2[文件描述符]
E --> E3[信号处理]
B --> F1[独立 task_struct]
C --> F2[独立 task_struct]
D --> F3[独立 task_struct]
style A fill:#ffcccc
style E fill:#ccffcc
style F1 fill:#ccccff
style F2 fill:#ccccff
style F3 fill:#ccccff
LWP 的特点:
- 每个线程在内核中都有一个
task_struct - 共享进程的地址空间(mm_struct)
- 独立的栈和寄存器
- 可以独立调度
clone() 系统调用
Linux 使用 clone() 系统调用创建线程:
1 |
|
关键 flags:
CLONE_VM:共享地址空间(线程)CLONE_FILES:共享文件描述符表CLONE_SIGHAND:共享信号处理CLONE_THREAD:属于同一线程组
示例:
1 | // pthread_create() 内部使用 clone() |
查看线程实现
查看线程信息
1 | # 查看进程的所有线程(LWP) |
查看线程和进程的关系
1 | # 查看进程的线程组 |
线程模型的对比
| 特性 | 一对一模型 | 多对一模型 | 多对多模型 |
|---|---|---|---|
| 并行性 | 高(真正并行) | 低(无法并行) | 高(可并行) |
| 切换开销 | 较大 | 小 | 中等 |
| 阻塞影响 | 无(独立线程) | 有(整个进程) | 无(独立线程) |
| 实现复杂度 | 中等 | 简单 | 复杂 |
| 多核利用 | 是 | 否 | 是 |
| 典型实现 | Linux NPTL, Windows | GNU Pth(已废弃) | Solaris |
Linux 线程实现的历史
LinuxThreads(已废弃)
- 模型:多对一模型(部分实现)
- 问题:
- 信号处理问题
- 进程 ID 和线程 ID 混乱
- 性能问题
- POSIX 兼容性问题
NPTL(当前实现)
- 模型:一对一模型
- 优势:
- 真正的并行执行
- 更好的 POSIX 兼容性
- 更好的性能
- 更好的信号处理
查看 NPTL 版本:
1 | # 查看 glibc 版本(包含 NPTL) |
线程的同步
互斥锁(Mutex)
互斥锁用于保护共享资源:
1 |
|
条件变量(Condition Variable)
条件变量用于线程间通信:
1 |
|
信号量(Semaphore)
信号量用于控制资源访问:
1 |
|
进程 vs 线程
详细对比
| 特性 | 进程 | 线程 |
|---|---|---|
| 定义 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 地址空间 | 独立地址空间 | 共享进程地址空间 |
| 资源 | 独立资源(文件、信号等) | 共享进程资源 |
| 通信方式 | IPC(管道、共享内存等) | 共享内存(直接访问) |
| 切换开销 | 大(切换页表、刷新 TLB) | 小(只需保存寄存器) |
| 创建开销 | 大(复制地址空间) | 小(共享地址空间) |
| 独立性 | 高(进程崩溃不影响其他进程) | 低(线程崩溃可能影响整个进程) |
| 同步 | 不需要(天然隔离) | 需要(共享资源需要同步) |
| 适用场景 | 需要隔离的任务 | 需要协作的任务 |
内存对比
graph TB
subgraph Process["进程(独立地址空间)"]
P1[进程1
代码段
数据段
堆
栈]
P2[进程2
代码段
数据段
堆
栈]
end
subgraph Thread["线程(共享地址空间)"]
T1[线程1
栈1]
T2[线程2
栈2]
T3[线程3
栈3]
Shared[共享
代码段
数据段
堆]
end
T1 --> Shared
T2 --> Shared
T3 --> Shared
style P1 fill:#ffcccc
style P2 fill:#ffcccc
style Shared fill:#ccffcc
style T1 fill:#ccccff
style T2 fill:#ccccff
style T3 fill:#ccccff
切换开销对比
进程切换
flowchart TD
A[保存当前进程上下文] --> B[切换页表]
B --> C[刷新 TLB]
C --> D[切换内核栈]
D --> E[恢复新进程上下文]
E --> F[切换完成]
Note over A,F: 开销大
通常需要微秒级时间
style A fill:#ffcccc
style F fill:#ccffcc
线程切换
flowchart TD
A[保存寄存器] --> B[切换栈指针]
B --> C[恢复寄存器]
C --> D[切换完成]
Note over A,D: 开销小
通常需要纳秒级时间
style A fill:#ccffcc
style D fill:#90EE90
使用场景
适合使用进程的场景
需要隔离的任务
- Web 服务器(每个请求一个进程)
- 系统服务(相互独立)
- 安全要求高的应用
需要利用多核 CPU
- CPU 密集型任务
- 并行计算
- 批处理任务
容错性要求高
- 一个任务失败不影响其他任务
- 需要进程级别的隔离
适合使用线程的场景
需要协作的任务
- 多线程服务器(共享连接池)
- GUI 应用(UI 线程 + 工作线程)
- 生产者-消费者模式
I/O 密集型任务
- 网络 I/O(多线程处理连接)
- 文件 I/O(并行读写)
- 数据库操作
需要共享数据
- 共享缓存
- 共享数据结构
- 实时协作
线程分类
用户线程
内核线程
实际应用示例
示例1:多进程 Web 服务器
1 |
|
示例2:多线程 Web 服务器
1 |
|
性能考虑
进程 vs 线程性能对比
| 操作 | 进程 | 线程 |
|---|---|---|
| 创建时间 | 较慢(毫秒级) | 较快(微秒级) |
| 切换时间 | 较慢(微秒级) | 较快(纳秒级) |
| 内存占用 | 较大(独立地址空间) | 较小(共享地址空间) |
| 通信开销 | 较大(需要 IPC) | 较小(直接访问) |
选择建议
flowchart TD
A[需要并发处理] --> B{需要隔离?}
B -->|是| C[使用进程]
B -->|否| D{需要共享数据?}
D -->|是| E[使用线程]
D -->|否| F{CPU 密集型?}
F -->|是| C
F -->|否| G{I/O 密集型?}
G -->|是| E
G -->|否| H[根据具体情况选择]
style C fill:#ffcccc
style E fill:#ccffcc
查看进程和线程
查看进程
1 | # 查看所有进程 |
查看线程
1 | # 查看线程(ps -T 或 ps -eLf) |
查看进程和线程数量
1 | # 查看进程数量 |
常见问题
问题1:进程 vs 线程的选择
选择进程的情况:
- 需要强隔离
- 需要容错性
- CPU 密集型任务
- 需要利用多核
选择线程的情况:
- 需要共享数据
- I/O 密集型任务
- 需要快速通信
- 资源受限
问题2:多进程 vs 多线程的性能
多进程优势:
- 更好的隔离性
- 更好的容错性
- 可以利用多核 CPU
多线程优势:
- 更低的创建和切换开销
- 更快的通信速度
- 更少的内存占用
问题3:线程安全问题
问题:
- 多个线程访问共享数据
- 可能导致数据竞争
- 需要同步机制
解决方案:
- 使用互斥锁
- 使用条件变量
- 使用原子操作
- 使用无锁数据结构
最佳实践
1. 进程设计
- 合理使用进程池:避免频繁创建/销毁进程
- 进程间通信:选择合适的 IPC 方式
- 资源管理:及时清理子进程,避免僵尸进程
- 错误处理:正确处理 fork() 失败等情况
2. 线程设计
- 线程数量:不要创建过多线程(通常 CPU 核心数 × 2)
- 线程同步:正确使用锁和同步原语
- 避免死锁:按顺序获取锁,使用超时机制
- 线程安全:确保共享数据的线程安全
3. 混合使用
在实际应用中,可以混合使用进程和线程:
graph TB
A[主进程] --> B[工作进程1]
A --> C[工作进程2]
A --> D[工作进程3]
B --> E[线程池]
C --> F[线程池]
D --> G[线程池]
style A fill:#ffcccc
style B fill:#ccffcc
style C fill:#ccffcc
style D fill:#ccffcc
示例:
- 主进程管理多个工作进程
- 每个工作进程使用线程池处理任务
- 结合进程隔离和线程效率的优势
总结
进程和线程是操作系统的核心概念:
核心要点
- 进程:资源分配的基本单位,独立地址空间
- 线程:CPU 调度的基本单位,共享地址空间
- 选择:根据需求选择进程或线程
- 性能:线程开销小,但需要同步
- 隔离:进程隔离性好,线程需要同步
使用建议
- 需要隔离:使用进程
- 需要协作:使用线程
- CPU 密集型:使用进程或多线程
- I/O 密集型:使用多线程
- 混合场景:进程 + 线程池
理解进程和线程的区别和特点,对于系统设计、性能优化和问题诊断都非常重要。