很多人都知道“进程是资源分配的基本单位,线程是 CPU 调度的基本单位”,但如果继续追问: 进程到底拥有哪些资源?线程之间共享什么?为什么线程切换更快?Linux 内核又是如何实现线程的? 很多人就不一定能完整回答出来了。
本文将从概念、内存模型、资源共享、创建方式、通信方式、上下文切换以及实际应用场景等方面,系统讲清楚 Linux 中进程与线程的区别。
一、先看结论
可以先用一句话理解:
- 进程是资源容器,强调隔离
- 线程是进程中的执行单元,强调协作
生活化理解:
- 一个进程就像一套独立的房子
- 同一个进程中的多个线程,就像房子里一起工作的多个人
- 不同房子之间彼此隔离
- 同一套房子里的人共享客厅、厨房和物品,但每个人有自己的工作状态

二、什么是进程
进程可以理解为:
程序的一次运行实例。
程序本身只是存储在磁盘上的可执行文件,例如:
/usr/bin/nginx
/usr/bin/python3
/usr/bin/java
当操作系统加载程序并开始执行后,就会创建一个进程。
例如执行:
python3 app.py
此时,磁盘上的 python3 是程序,而正在运行的 Python 实例是进程。
同一个程序可以同时启动多个进程。例如:
python3 app.py
python3 app.py
python3 app.py
虽然三个进程运行的是同一份程序代码,但它们拥有不同的进程 ID 和独立的运行环境。
三、一个进程包含哪些内容
Linux 中的一个进程通常包含以下资源。
1. 进程 ID
每个进程都有一个唯一的进程 ID,也就是 PID。
可以使用以下命令查看:
ps -ef
或者:
ps aux
输出示例:
root 1024 1 0 10:20 ? 00:00:02 nginx
www-data 1025 1024 0 10:20 ? 00:00:01 nginx
其中:
- 1024 是主进程 PID
- 1025 是工作进程 PID
- 每个进程都有自己独立的编号
2. 虚拟地址空间
每个进程都拥有自己的虚拟地址空间。
典型的进程地址空间包括:
高地址
┌──────────────────┐
│ 栈 │
├──────────────────┤
│ │
│ 内存映射区域 │
│ │
├──────────────────┤
│ 堆 │
├──────────────────┤
│ 数据段 │
├──────────────────┤
│ 代码段 │
└──────────────────┘
低地址
各区域作用如下。
代码段
代码段保存程序执行的机器指令。
例如下面的函数代码:
int add(int a, int b)
{
return a + b;
}
编译后对应的机器指令通常存放在代码段中。
代码段一般具有只读和可执行属性。
数据段
数据段主要保存已经初始化的全局变量和静态变量。
int global_value = 100;
static int count = 10;
BSS 段
BSS 段主要保存未初始化或初始化为零的全局变量和静态变量。
int global_count;
static int status;
堆
堆主要用于动态内存分配。
int *data = malloc(sizeof(int) * 100);
在 C++ 中,使用 new 申请的对象通常也存放在堆中。
User *user = new User();
栈
栈主要保存:
- 函数参数
- 局部变量
- 函数返回地址
- 部分寄存器信息
- 函数调用上下文
例如:
void test()
{
int number = 10;
}
局部变量 number 通常存放在线程栈中。

3. 文件描述符
进程打开文件、网络连接、管道或设备后,Linux 会为其分配文件描述符。
常见文件描述符:
0:标准输入 stdin
1:标准输出 stdout
2:标准错误 stderr
例如:
int fd = open("data.txt", O_RDONLY);
fd 就是文件描述符。
需要注意的是,文件描述符只是进程内部的一个整数索引,真正的文件对象由内核维护。
4. 信号处理方式
Linux 使用信号通知进程发生了某些事件。
常见信号:
SIGINT 用户按下 Ctrl+C
SIGTERM 请求进程正常退出
SIGKILL 强制终止进程
SIGSEGV 非法内存访问
SIGCHLD 子进程状态发生变化
发送信号:
kill -15 1234
强制终止进程:
kill -9 1234
其中,1234 是目标进程的 PID。
5. 用户和权限信息
每个进程都有对应的用户和用户组信息,例如:
- 实际用户 ID
- 有效用户 ID
- 实际组 ID
- 有效组 ID
- 附加组信息
这些信息决定进程可以访问哪些文件、设备和系统资源。
6. 调度信息
Linux 内核还会维护进程或线程的调度信息,例如:
- 当前运行状态
- 调度策略
- 优先级
- CPU 时间
- 当前运行的 CPU 核心
- 上下文信息
四、什么是线程
线程可以理解为:
进程内部的一条执行流。
一个进程至少包含一个线程,也就是主线程。
如果一个进程创建了更多线程,那么这些线程可以在同一个进程中并发执行不同任务。
例如一个网络服务器可能包含:
主线程:接收客户端连接
线程 1:处理客户端 A 请求
线程 2:处理客户端 B 请求
线程 3:执行日志写入
线程 4:执行定时任务
这些线程属于同一个进程,共享进程的大部分资源。

五、线程拥有自己的哪些资源
同一个进程中的线程虽然会共享大量资源,但每个线程也有自己的私有执行上下文。
1. 线程 ID
每个线程都有自己的线程 ID。
在 POSIX 线程库中,可以使用:
pthread_self();
获取当前线程标识。
在 Linux 内核层面,每个可调度任务都有对应的任务 ID。
2. 程序计数器
程序计数器记录线程下一条需要执行的指令地址。
不同线程可能执行不同函数,因此每个线程必须拥有自己的程序计数器。
3. 寄存器上下文
CPU 在执行线程时,会使用多个寄存器保存计算状态。
当线程被切换出去时,内核需要保存它的寄存器信息;当线程再次运行时,再恢复这些寄存器信息。
4. 线程栈
每个线程都有自己独立的栈。
例如:
void *worker(void *arg)
{
int count = 100;
char buffer[1024];
return NULL;
}
count 和 buffer 是线程函数中的局部变量,通常存放在当前线程的栈中。
多个线程不能共用同一个栈,否则不同函数调用之间会发生严重冲突。
5. 调度状态和优先级
每个线程都可以被独立调度,因此不同线程可能处于不同状态:
- 一个线程正在 CPU 上运行
- 一个线程正在等待网络数据
- 一个线程正在等待互斥锁
- 一个线程正在休眠
六、同一个进程中的线程共享什么
同一进程中的线程通常共享以下资源。
1. 代码段
所有线程执行的是同一进程中的程序代码。
不同线程可以执行相同函数,也可以执行不同函数。
2. 数据段
全局变量和静态变量通常由同一进程中的所有线程共享。
例如:
int global_count = 0;
多个线程都可以访问和修改 global_count。
这也会带来线程安全问题。
例如两个线程同时执行:
global_count++;
最终结果可能不是预期值,因为 global_count++ 并不是一个不可分割的原子操作。
3. 堆内存
通过 malloc 或 new 申请的堆内存通常可以被同一进程中的多个线程访问。
int *shared_data = malloc(sizeof(int) * 100);
只要其他线程获得了 shared_data 的地址,就可以访问这块内存。
4. 打开的文件
同一进程中的线程共享文件描述符表。
如果一个线程打开文件:
int fd = open("log.txt", O_WRONLY);
其他线程通常也可以使用同一个 fd 写入文件。
5. 当前工作目录
同一进程中的线程通常共享当前工作目录。
如果一个线程调用:
chdir("/tmp");
可能会影响同一进程中的其他线程。
6. 信号处理方式
同一进程中的线程通常共享信号处理函数,但每个线程可以有自己的信号屏蔽集合。
七、进程和线程的核心区别
| 基本定义 | 程序的一次运行实例 | 进程中的一条执行流 |
| 资源关系 | 拥有相对独立的资源 | 共享所属进程的大部分资源 |
| 地址空间 | 不同进程地址空间相互隔离 | 同一进程内线程共享地址空间 |
| CPU 调度 | Linux 实际调度可执行任务 | 线程是常见的调度执行单位 |
| 创建开销 | 相对较大 | 相对较小 |
| 切换开销 | 通常相对较大 | 通常相对较小 |
| 通信方式 | 管道、消息队列、共享内存、Socket 等 | 直接访问共享内存 |
| 数据安全 | 隔离性较好 | 容易发生数据竞争 |
| 稳定性 | 一个进程崩溃通常不直接影响其他进程 | 一个线程崩溃可能导致整个进程退出 |
| 适用场景 | 服务隔离、独立任务、安全边界 | 高并发、任务协作、共享数据 |
需要注意:
“进程是资源分配的基本单位,线程是 CPU 调度的基本单位”是一种便于理解的操作系统概念总结。
在 Linux 内核中,进程和线程的底层实现非常接近。它们都可以使用内核中的任务结构进行描述,主要区别在于彼此之间共享了哪些资源。
八、为什么线程比进程更轻量
线程通常被称为轻量级执行单元,主要有以下几个原因。
1. 创建线程时共享现有资源
创建新进程时,需要建立相对独立的进程运行环境。
创建线程时,可以直接共享当前进程的:
- 地址空间
- 代码段
- 数据段
- 堆
- 文件描述符
- 大部分系统资源
因此创建线程通常比创建进程更轻量。
2. 线程通信更直接
同一进程中的线程可以直接通过共享变量通信。
int task_count = 0;
不同线程可以直接访问 task_count。
而不同进程不能直接访问彼此的普通内存,需要使用进程间通信机制。
3. 上下文切换成本通常更低
进程切换时,往往涉及更多地址空间和内存映射相关状态的切换。
线程切换时,如果两个线程属于同一个进程,它们共享地址空间,因此需要切换的资源通常更少。
不过,线程切换仍然不是免费的,仍然需要保存和恢复:
- CPU 寄存器
- 程序计数器
- 栈指针
- 调度状态
- 其他执行上下文
线程数量也不是越多越好。
九、进程是如何创建的
Linux 中常见的进程创建方式是 fork()。
1. 使用 fork 创建子进程
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
return 1;
}
if (pid == 0)
{
printf("我是子进程,PID=%d\\n", getpid());
}
else
{
printf("我是父进程,PID=%d,子进程 PID=%d\\n",
getpid(), pid);
}
return 0;
}
fork() 调用成功后,会出现两个执行流:
- 父进程中的 fork() 返回子进程 PID
- 子进程中的 fork() 返回 0
2. 使用 exec 执行新程序
fork() 创建子进程后,通常会调用 exec 系列函数加载新程序。
#include <unistd.h>
#include <stdio.h>
int main()
{
execl("/bin/ls", "ls", "-l", NULL);
perror("execl");
return 1;
}
exec 不会再创建一个新的进程,而是使用新程序替换当前进程的代码和数据。
常见组合:
fork 创建子进程
↓
子进程调用 exec
↓
子进程执行新的程序
Shell 执行命令时经常使用类似机制。
十、线程是如何创建的
Linux 用户空间程序通常使用 POSIX Threads,也就是 pthread。
1. pthread_create 创建线程
#include <stdio.h>
#include <pthread.h>
void *worker(void *arg)
{
const char *name = (const char *)arg;
printf("线程开始执行:%s\\n", name);
return NULL;
}
int main()
{
pthread_t thread;
int result = pthread_create(
&thread,
NULL,
worker,
"worker-1"
);
if (result != 0)
{
printf("线程创建失败\\n");
return 1;
}
pthread_join(thread, NULL);
printf("主线程结束\\n");
return 0;
}
编译命令:
gcc thread_demo.c -o thread_demo -pthread
运行:
./thread_demo
2. pthread_join 等待线程结束
pthread_join(thread, NULL);
该函数会阻塞当前线程,直到目标线程执行结束。
它的作用类似于等待子进程结束,但它等待的是线程。
3. pthread_exit 退出线程
线程可以主动调用:
pthread_exit(NULL);
结束当前线程。
它只结束当前线程,不一定会结束整个进程。
但是如果主线程直接从 main 返回,整个进程通常会结束,其他线程也会被终止。
十一、线程为什么容易出现安全问题
多线程最常见的问题是数据竞争。
假设多个线程共享一个全局变量:
int count = 0;
两个线程都执行:
count++;
表面上看只是一条语句,但底层可能分为:
1. 从内存读取 count
2. 对 count 加 1
3. 将新值写回内存
可能出现以下执行顺序:
线程 A 读取 count,值为 10
线程 B 读取 count,值为 10
线程 A 写入 11
线程 B 写入 11
执行了两次加一,结果却只增加了一次。
这就是典型的数据竞争。
十二、使用互斥锁保护共享数据
可以使用互斥锁确保同一时刻只有一个线程访问临界区。
#include <stdio.h>
#include <pthread.h>
#define LOOP_COUNT 100000
int count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *worker(void *arg)
{
for (int i = 0; i < LOOP_COUNT; i++)
{
pthread_mutex_lock(&mutex);
count++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t thread1;
pthread_t thread2;
pthread_create(&thread1, NULL, worker, NULL);
pthread_create(&thread2, NULL, worker, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("count = %d\\n", count);
pthread_mutex_destroy(&mutex);
return 0;
}
理论输出:
count = 200000
互斥锁保护的代码区域称为临界区。
需要注意,锁的范围不应该过大,否则会降低程序并发能力。
十三、进程之间如何通信
不同进程拥有相互隔离的虚拟地址空间,不能直接访问对方的普通变量。
因此需要使用 IPC,也就是进程间通信机制。
常见 IPC 方式包括:
1. 管道
父进程 → 管道 → 子进程
适合具有亲缘关系的进程进行简单通信。
Shell 中的管道:
ps aux | grep nginx
ps aux 的输出会通过管道传给 grep nginx。
2. 命名管道
命名管道也叫 FIFO。
mkfifo message_pipe
不同进程可以通过指定的 FIFO 文件通信。
3. 消息队列
消息队列允许进程按照消息结构发送和接收数据。
适合异步通信。
4. 共享内存
共享内存允许多个进程映射同一块物理内存。
它通常是非常高效的 IPC 方式。
但是多个进程同时访问共享内存时,也需要配合:
- 信号量
- 互斥锁
- 文件锁
- 原子操作
否则同样可能发生数据竞争。
5. Socket
Socket 不仅可以用于不同计算机之间的网络通信,也可以用于同一台计算机中的进程通信。
常见方式:
TCP Socket
UDP Socket
Unix Domain Socket
Unix Domain Socket 在本机进程通信中非常常见,例如 Docker、MySQL 和很多后台服务都会使用。
6. 信号
信号主要用于事件通知。
例如:
kill -TERM 1234
向进程发送终止请求。
信号适合传递事件,不适合传输大量业务数据。
十四、线程之间如何通信
同一进程中的线程共享地址空间,因此线程之间可以直接使用共享变量通信。
例如:
int task_ready = 0;
一个线程修改:
task_ready = 1;
另一个线程读取:
if (task_ready)
{
// 开始处理任务
}
但直接使用共享变量可能出现:
- 数据竞争
- 内存可见性问题
- 指令重排序问题
- 死锁
- 活锁
- 饥饿
因此通常需要配合同步机制:
互斥锁 mutex
读写锁 rwlock
条件变量 condition variable
信号量 semaphore
自旋锁 spinlock
原子操作 atomic
屏障 barrier
十五、进程和线程的上下文切换
操作系统为了让多个任务共享 CPU,会不断切换正在运行的任务。
这个过程称为上下文切换。
进程切换可能涉及
- 保存当前 CPU 寄存器
- 保存程序计数器
- 保存栈指针
- 切换调度状态
- 切换地址空间相关信息
- 切换页表相关上下文
- 重新建立部分缓存状态
线程切换可能涉及
- 保存和恢复寄存器
- 保存和恢复程序计数器
- 保存和恢复栈指针
- 切换调度状态
- 切换线程局部数据
如果切换的两个线程属于同一个进程,它们共享地址空间,因此通常不需要完整切换地址空间。
这也是线程切换一般比进程切换更轻量的原因之一。
但需要注意:
切换成本与操作系统实现、CPU 架构、缓存状态、线程所属进程以及任务工作集等因素有关,不能简单理解为线程切换永远比进程切换快。
十六、Linux 内核中的进程和线程
Linux 对进程和线程的实现方式比较特别。
在 Linux 内核中,进程和线程都可以看作可调度的任务。
它们的核心差异主要体现在资源共享程度上。
创建任务时,可以通过不同参数决定是否共享:
- 地址空间
- 文件描述符表
- 文件系统信息
- 信号处理方式
- 其他资源
从这个角度看:
进程:共享资源较少的任务
线程:共享资源较多的任务
Linux 用户空间的 pthread_create() 最终会通过底层系统调用创建一个共享当前进程资源的新任务。
因此,Linux 中的线程也经常被称为轻量级进程。
十七、如何查看 Linux 进程和线程
1. 查看进程
ps -ef
或者:
ps aux
2. 动态查看进程
top
也可以使用:
htop
3. 查看某个进程的线程
ps -T -p 进程PID
例如:
ps -T -p 1234
4. 使用 top 查看线程
启动 top 后按:
H
可以切换线程显示模式。
也可以直接执行:
top -H
查看指定进程中的线程:
top -H -p 1234
5. 查看进程状态
cat /proc/1234/status
其中 1234 是 PID。
常见字段:
Name
State
Pid
PPid
Threads
VmSize
VmRSS
Threads 表示当前进程包含的线程数量。
6. 查看进程文件描述符
ls -l /proc/1234/fd
7. 查看进程内存映射
cat /proc/1234/maps
或者:
pmap 1234
十八、进程和线程分别适合什么场景
适合使用多进程的场景
1. 需要故障隔离
例如浏览器的不同标签页使用不同进程。
一个标签页崩溃时,不希望整个浏览器一起崩溃。
2. 需要安全隔离
不同进程拥有独立地址空间,更适合作为安全边界。
3. 独立服务
例如:
用户服务
订单服务
支付服务
日志服务
不同服务可以部署为独立进程。
4. CPU 密集型任务
在某些语言运行时中,多进程可以绕过运行时级别的线程限制。
例如 Python 的 CPU 密集型任务有时会选择多进程。
适合使用多线程的场景
1. 网络服务器
多个线程可以同时处理多个客户端连接。
2. GUI 程序
主线程负责界面事件,工作线程负责耗时任务。
3. 线程池
预先创建固定数量的线程,循环处理任务,避免频繁创建和销毁线程。
4. 数据共享频繁
如果多个执行任务需要频繁共享大量数据,线程通常比进程通信更方便。
5. I/O 密集型任务
例如:
- 网络请求
- 磁盘读写
- 数据库访问
- 消息队列消费
当一个线程等待 I/O 时,其他线程可以继续运行。
十九、实际项目中应该怎么选
不能简单地说多进程一定更好,或者多线程一定更快。
应该根据实际需求选择。
优先考虑多进程
当系统更关注:
- 隔离性
- 稳定性
- 安全性
- 独立部署
- 故障不扩散
可以优先考虑多进程。
优先考虑多线程
当系统更关注:
- 高并发
- 快速通信
- 数据共享
- 较低创建成本
- 任务协作
可以优先考虑多线程。
实际工程中经常组合使用
很多系统采用:
多进程 + 每个进程内部多线程
例如一个服务器可以启动多个工作进程,每个工作进程内部再创建线程池。
这样既可以获得一定的进程隔离能力,又可以获得线程协作和并发处理能力。
二十、常见面试题
1. 进程和线程最大的区别是什么
进程拥有相对独立的地址空间和资源,线程共享所属进程的大部分资源。
进程强调隔离,线程强调协作。
2. 线程共享哪些资源
同一进程中的线程通常共享:
- 代码段
- 数据段
- 堆
- 文件描述符
- 当前工作目录
- 用户和权限信息
- 大部分信号处理配置
3. 线程私有的资源有哪些
每个线程通常拥有:
- 线程 ID
- 程序计数器
- CPU 寄存器上下文
- 线程栈
- 调度状态
- 线程局部存储
- 部分信号屏蔽信息
4. 为什么线程切换通常比进程切换快
同一进程中的线程共享地址空间,因此线程切换时通常不需要完整切换地址空间和页表相关上下文,需要切换的资源更少。
5. 多线程一定比单线程快吗
不一定。
线程过多可能导致:
- 频繁上下文切换
- 锁竞争
- CPU 缓存失效
- 内存消耗增加
- 调度开销增加
- 程序复杂度提高
如果任务量很小,或者任务之间存在严重锁竞争,多线程可能比单线程更慢。
6. 一个线程崩溃会影响整个进程吗
有可能。
线程共享进程地址空间,如果某个线程发生非法内存访问,例如触发 SIGSEGV,通常可能导致整个进程终止。
7. 进程间可以共享内存吗
可以。
虽然不同进程默认拥有独立地址空间,但可以使用共享内存 IPC,将同一块物理内存映射到多个进程的地址空间中。
8. fork 之后父子进程内存是否完全复制
从逻辑上看,父子进程拥有各自独立的地址空间。
但 Linux 通常使用写时复制,也就是 Copy-On-Write。
刚执行 fork() 时,父子进程可以暂时共享物理内存页面。
当其中一个进程尝试修改某个页面时,内核才会复制该页面。
这样可以减少不必要的内存复制,提高 fork() 的效率。
二十一、总结
Linux 中进程与线程的核心区别,可以总结为以下几点。
进程
- 是程序的一次运行实例
- 拥有相对独立的地址空间
- 是系统资源的容器
- 隔离性和稳定性较好
- 创建和切换开销通常较大
- 进程之间需要通过 IPC 通信
线程
- 是进程中的执行流
- 是 CPU 调度的重要执行单位
- 共享所属进程的大部分资源
- 每个线程拥有独立的栈和寄存器上下文
- 创建和切换开销通常较小
- 线程之间通信方便,但需要处理同步问题
最后用一句话总结:
进程重隔离,线程重协作;进程是资源容器,线程是执行单元。
在实际开发中,不应该机械地选择多进程或多线程,而应该综合考虑:
- 系统稳定性
- 故障隔离
- 并发规模
- 数据共享频率
- 通信成本
- 开发复杂度
- 运行环境和语言特性
理解进程和线程,不只是为了应付面试,更是为了正确设计高并发服务器、线程池、任务调度系统、微服务以及后台程序。
参考命令汇总
# 查看进程
ps -ef
ps aux
# 动态查看进程
top
htop
# 查看线程
ps -T -p PID
top -H
top -H -p PID
# 查看进程状态
cat /proc/PID/status
# 查看文件描述符
ls -l /proc/PID/fd
# 查看内存映射
cat /proc/PID/maps
pmap PID
# 给进程发送信号
kill -15 PID
# 强制结束进程
kill -9 PID
推荐标签
Linux
操作系统
进程
线程
多线程
并发编程
Linux内核
后端开发
程序员面试
C语言




