进程与线程:一篇搞懂核心区别与 Linux 实现
2026/9/1 15:23:50 网站建设 项目流程

一、进程

磁盘上的可执行文件本身只是一个静态程序

例如:

./test

当我们执行它以后,操作系统会为程序创建运行环境,包括:

  • 建立虚拟地址空间

  • 加载代码和数据

  • 创建栈、堆等内存区域

  • 建立文件描述符等内核资源

  • 创建用于调度和管理的内核数据结构

这时,静态程序才真正变成一个运行中的进程

可以简单理解:

程序 + 运行所需资源 = 进程

一个进程通常拥有自己的:

  • 虚拟地址空间

  • 文件描述符

  • 内存映射

  • 信号处理信息

  • 工作目录

  • 用户权限

  • PID 等资源

不同进程的用户地址空间默认相互隔离,因此一个进程不能直接通过普通指针访问另一个进程的数据。

如果进程之间需要交换数据,一般需要使用 IPC:

管道 FIFO 共享内存 消息队列 Socket 信号

二、线程

线程可以理解为:

进程内部的一条执行流,也是 CPU 调度的重要单位。

一个进程至少存在一个线程,即主线程。

例如:

int main() { while (1) { } }

运行以后可以理解为:

进程 P └── 主线程

如果创建多个线程:

std::thread t1(func); std::thread t2(func);

那么:

进程 P │ ├── 主线程 ├── 线程 1 └── 线程 2

虽然存在多条执行流,但它们仍然属于同一个进程

因此可以先记住一句话:

进程偏向资源管理 线程偏向程序执行

三、共享资源

同一个进程中的线程不会各自复制一整套进程资源。

它们通常共享:

代码段 数据段 全局变量 静态变量 堆 mmap 映射区域 虚拟地址空间 文件描述符表

但每个线程必须拥有自己的:

线程 ID 用户栈 内核栈 CPU 寄存器现场 程序计数器 PC 调度状态

可以画成:

一个进程 │ ┌────────────┼────────────┐ │ │ │ 线程1 线程2 线程3 │ │ │ 栈1 栈2 栈3 │ │ │ PC1 PC2 PC3 共同共享 │ ┌───────────┼───────────┐ │ │ │ 代码 堆 mmap │ 全局数据

最重要的一句话:

线程共享进程资源,但保留独立的执行现场。

Linux 中task_struct里一般保存什么

在 Linux 中,无论是进程还是线程,本质上都对应一个可以被调度的task,每个 task 都拥有自己的task_struct。需要注意的是,task_struct中没有单独叫做LWP的字段,线程的 LWP 本质上就是线程 ID,也就是该 task 的pid;而同一个进程中的多个线程拥有相同的tgid,因此可以简单理解为:pid更接近线程 TID/LWP,tgid更接近我们平时看到的进程 PID,主线程满足pid == tgidtask_struct本身可以看成 Linux 内核中一个执行流的“总档案”,其中通常包含线程身份信息、调度信息、内存信息、文件信息、信号信息以及执行现场等,例如:

struct task_struct { pid_t pid; // 当前 task 的 TID / LWP pid_t tgid; // 所属进程的 PID struct task_struct *parent; struct task_struct *group_leader; int prio; // 调度优先级 unsigned int policy; // 调度策略 struct sched_entity se; // 调度实体 struct mm_struct *mm; // 虚拟地址空间 struct files_struct *files; // 文件描述符表 struct fs_struct *fs; // 工作目录等文件系统信息 struct signal_struct *signal; struct sighand_struct *sighand; struct cred *cred; // 用户权限信息 void *stack; // 内核栈 // 还有 cgroup、namespace、CPU affinity、 // 时间统计、ptrace、seccomp 等大量信息 };

不同线程拥有不同的task_structpid、栈和调度现场,但同一进程中的线程可以让mmfiles等指针指向同一个对象,从而共享虚拟地址空间、堆、文件描述符等资源。因此 Linux 中所谓“线程比进程轻量”,并不是线程没有自己的 PCB,而是它虽然有独立的task_struct,却共享了大量进程级资源。

四、线程栈

线程的栈保存:

  • 局部变量

  • 函数参数

  • 返回地址

  • 函数调用现场

假设:

线程1 → func1() 线程2 → func2()

两个线程的函数调用过程完全可能不同。

如果它们共用同一个普通线程栈,那么:

局部变量 返回地址 函数调用现场

都会互相覆盖,程序会立即混乱。

因此:

线程1 → 用户栈1 线程2 → 用户栈2 线程3 → 用户栈3

教材中有时会出现“进程栈”这个说法。

对于单线程进程,可以粗略认为:

进程栈 ≈ 主线程栈

但从严格意义上说:

栈属于线程,而不是整个进程只有一份栈。


五、“线程堆”

通常没有所谓的“线程堆”。

同一个进程中的线程共享整个虚拟地址空间,因此也共享:

Heap

例如:

int* p = new int(10);

如果线程 A 得到了p,线程 B 也拿到了这个地址,那么两个线程都可以访问这块内存。

因此:

进程虚拟地址空间 代码段 ← 共享 数据段 ← 共享 堆 Heap ← 共享 mmap ← 共享 线程1栈 ← 独立 线程2栈 ← 独立 线程3栈 ← 独立

正因为线程共享堆、全局变量等资源,多线程程序才容易发生:

数据竞争 Use After Free 竞态条件

所以通常需要:

mutex semaphore spinlock atomic

进行同步。


六、Linux 中进程和线程是怎么实现的

传统操作系统中经常使用:

PCB Process Control Block 进程控制块

来描述进程。

Linux 中一个非常重要的数据结构是:

struct task_struct

可以把它粗略理解为 Linux 中描述一个可调度任务的数据结构。

这里有一个非常关键的知识点:

Linux 中每一个线程都有自己的 task_struct。

例如一个进程有三个线程:

线程 T1 → task_struct A 线程 T2 → task_struct B 线程 T3 → task_struct C

因此 Linux 更接近于:

一个可独立调度的执行流 ↓ task ↓ task_struct

而不是:

一个进程 ↓ 一个 task_struct ↓ 里面再装很多线程

这也是理解 Linux 线程模型的关键。


七、同一个进程的线程共享资源

虽然每个线程都有自己的task_struct,但它们可以让其中的一些资源指针指向同一个对象。

例如虚拟地址空间:

task_struct A ──┐ │ task_struct B ──┼──→ 同一个 mm_struct │ task_struct C ──┘

mm_struct用来描述进程的虚拟地址空间。

因此多个线程共享同一个mm_struct,自然就共享:

代码 数据 堆 mmap

文件描述符也是类似:

task_struct A ──┐ │ task_struct B ──┼──→ 同一个 files_struct │ task_struct C ──┘

所以线程 A:

int fd = open("a.txt", O_RDWR);

得到:

fd = 3

线程 B 如果知道3,一般也能够:

write(3, ...);

因为两个线程使用的是同一个文件描述符表。

因此 Linux 线程可以理解为:

拥有不同 task_struct,但共享大量进程级资源的 task。


八、线程为什么还有自己的内核栈

每个线程一般都有两种非常重要的栈:

用户栈 + 内核栈

当线程执行普通代码:

func();

运行在用户态,使用:

线程自己的用户栈

当线程执行:

read(fd, buf, 100);

进入系统调用:

用户态 ↓ 内核态 ↓ 执行 Linux 内核代码

这时内核需要保存自己的函数调用现场,因此会使用:

该线程对应的内核栈

所以:

线程 A ├── 用户栈 A └── 内核栈 A 线程 B ├── 用户栈 B └── 内核栈 B

即使它们属于同一个进程,也不能共用普通的线程栈。


九、为什么线程切换通常比进程切换轻

假设 CPU 从:

进程 A

切换到:

进程 B

两个进程通常拥有不同的:

mm_struct 页表 虚拟地址空间

因此除了切换寄存器、PC、调度状态之外,还可能涉及:

地址空间切换 页表切换 TLB 影响 Cache 影响

所以进程切换通常比较重。

而同一进程中的两个线程:

线程 A ──┐ ├──→ 同一个 mm_struct 线程 B ──┘

通常不需要切换整个地址空间。

主要切换:

寄存器 PC 栈 调度状态

因此一般来说:

线程切换成本 < 进程切换成本

注意这里只能说通常更低,并不是线程切换完全没有成本。


十、进程和线程各有什么优缺点

1. 进程隔离性更强

不同进程拥有独立虚拟地址空间。

例如进程 A 崩溃:

int* p = nullptr; *p = 10;

通常不会直接破坏进程 B 的用户空间。

因此进程的优势是:

隔离性强 安全性高 一个进程出错不容易直接污染另一个进程

2. 线程通信更加方便

线程共享同一个地址空间,因此可以直接共享变量:

int global = 10;

线程 A:

global = 20;

线程 B:

cout << global;

不需要像进程那样专门使用 IPC。

因此:

线程通信方便 创建和切换成本通常较低

但代价就是:

同步更加复杂 一个线程的内存错误可能影响整个进程

十一、fork 和创建线程的区别

Linux 中:

fork();

主要创建一个新的进程。

父进程 │ fork │ ┌─┴─┐ │ │ 父进程 子进程

父子进程在逻辑上拥有独立地址空间。

fork()时,Linux 通常通过:

COW Copy-On-Write 写时复制

暂时共享物理页。

但一旦发生写操作,就会逐渐分离。

因此逻辑上仍然是:

两个独立进程

而:

pthread_create();

创建的是同一进程中的新线程。

多个线程会共享:

地址空间 堆 文件描述符 mmap 全局变量
fork ↓ 创建独立进程 ↓ 资源相对独立 pthread_create ↓ 创建线程 ↓ 大量资源共享

十二、为什么 Linux 线程被称为轻量级进程

Linux 没有把“进程”和“线程”完全设计成两套毫无关系的对象。

内核统一把它们看成:

task

每个 task 都有自己的:

task_struct 调度信息 执行现场

区别主要体现在:

它们共享多少资源。

普通进程可能是:

task A → mm_struct A task B → mm_struct B

而同一个进程中的线程:

task A ──┐ ├──→ mm_struct X task B ──┘

文件、信号等资源也可以类似共享。

因此线程可以粗略理解成:

共享了大量资源的轻量级 task。

这也是 Linux 中“轻量级进程”这一说法的来源。


十三、进程与线程核心区别

对比项进程线程
核心作用资源管理、隔离CPU 执行流
地址空间不同进程独立同进程线程共享
代码/数据/堆不同进程通常独立同进程线程共享
独立每个线程独立
CPU 寄存器独立每个线程独立
PC独立每个线程独立
文件描述符不同进程通常分别维护同进程线程通常共享
通信通常需要 IPC可直接共享变量
创建成本较高较低
切换成本通常较高通常较低
隔离性
Linux task_struct每个 task 一个每个线程一个

十四、模型图

Linux 内核 task_struct A 线程 A │ │ ├──────────────┐ │ │ task_struct B │ 线程 B │ │ │ └──────┬───────┘ │ ┌─────────────┴─────────────┐ │ │ ▼ ▼ mm_struct files_struct │ │ 共享地址空间 共享文件表 │ ┌───────┼────────┐ │ │ │ 代码 数据 堆 │ mmap 线程 A 自己拥有: ├── 用户栈 A ├── 内核栈 A ├── PC └── CPU 寄存器现场 线程 B 自己拥有: ├── 用户栈 B ├── 内核栈 B ├── PC └── CPU 寄存器现场

总结

1. 进程是资源管理和隔离的基本单位,线程是 CPU 执行和调度的基本单位。

2. 同一进程中的线程共享地址空间、代码、数据、堆、mmap 和文件描述符等资源。

3. 每个线程都有自己独立的用户栈、内核栈、PC、寄存器现场和调度状态。

4. Linux 中每个线程都有自己的task_struct,同一进程的线程通过共享mm_structfiles_struct等资源形成线程组。

5. 线程通信方便、切换通常更轻,但隔离性更弱,也更容易产生数据竞争,因此需要同步机制。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询