一、进程
磁盘上的可执行文件本身只是一个静态程序。
例如:
./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 == tgid。task_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_struct、pid、栈和调度现场,但同一进程中的线程可以让mm、files等指针指向同一个对象,从而共享虚拟地址空间、堆、文件描述符等资源。因此 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_struct、files_struct等资源形成线程组。
5. 线程通信方便、切换通常更轻,但隔离性更弱,也更容易产生数据竞争,因此需要同步机制。