欢迎光临
我们一直在努力

Linux 进程和线程到底有什么区别?一文彻底讲清楚

很多人都知道“进程是资源分配的基本单位,线程是 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语言

赞(0)
未经允许不得转载:171主机测评 » Linux 进程和线程到底有什么区别?一文彻底讲清楚
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址