进程与线程
一、 进程 (Process):资源分配的最小单位
进程是操作系统对一个正在运行的程序的抽象。当你双击一个 .exe 文件时,操作系统就创建了一个进程。
1. 进程包含了什么?(工厂的基建设施)
每个进程都有自己独立的虚拟内存地址空间。在汇编层面,一个进程通常包含: * 代码段(Code Segment):存放编译后的二进制指令。 * 数据段(Data Segment):存放全局变量和静态变量。 * 堆(Heap):动态分配的内存。 * 栈(Stack):存放局部变量和函数调用信息。 * 资源句柄:如打开的文件、网络套接字、信号量等。
2. 核心特点:隔离性
- 各过各的:进程 A 不能直接访问进程 B 的内存。如果你想在汇编里直接修改另一个进程的变量,CPU 会触发“保护异常”。
- 崩溃不波离:一个进程崩溃了,通常不会影响到其他进程,因为它们的资源是物理隔离的。
二、 线程 (Thread):CPU 调度的最小单位
线程是进程内部的一个执行流。一个进程可以包含多个线程,它们共享进程的资源,但各自独立执行指令。
1. 线程共享什么?
同一个进程内的所有线程共享: * 代码段(大家执行同一份指令)。 * 数据段和堆(大家可以读写相同的全局变量)。 * 打开的文件。
2. 线程私有什么?
为了能独立跑起来,每个线程必须有自己的: * 寄存器集合(Register Set):包括 RIP (下一条指令地址)、RAX 等通用寄存器。 * 栈(Stack):每个线程有独立的局部变量空间,防止函数调用乱套。 * 状态标志位(RFLAGS)。
三、 进程 vs 线程:深度对比
| 维度 | 进程 | 线程 |
|---|---|---|
| 定义 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 内存 | 独立地址空间,互不干扰 | 共享所属进程的地址空间 |
| 开销 | 创建、销毁、切换开销大(要切页表) | 创建、销毁、切换开销小 |
| 通信 | 复杂(管道、信号、共享内存) | 简单(直接读写全局变量) |
| 可靠性 | 高(一个死,其他活) | 低(一个死,全家桶死) |
四、 从汇编和 CPU 的视角看
写汇编代码时,实际上是在操控线程的行为。
1. 上下文切换 (Context Switch)
CPU 的核心数量是有限的(比如 8 核),但电脑里可能有几百个线程在跑。CPU 快速在多个线程间轮换,这个过程叫切换。 * 步骤: 1. 保存当前线程的寄存器(RAX, RBX, RIP, RSP...)到内存。 2. 加载下一个线程之前保存的寄存器值。 3. 跳转到新的 RIP 指向的位置。 * 汇编意义:这意味着你不能假设寄存器的值在任何时候都是永久不变的,除非你在自己的代码块内连续运行。
2. 栈的独立性
这是汇编学习者的重点。
* 操作系统在创建线程时,会为每个线程分配一块独立的内存作为栈。
* RSP 寄存器会指向当前线程的栈顶。
* 如果你在多线程环境下写汇编,改变了 RSP 但没改回来,或者跳到了别人的栈空间,就会引发严重的错误。
3. 线程安全 (Thread Safety)
既然线程共享堆内存和全局变量,如果两个工人同时搬一个零件(两个线程同时修改同一个内存地址):
; 线程 1 执行: ; 线程 2 执行:
mov rax, [count] mov rax, [count]
add rax, 1 add rax, 1
mov [count], rax mov [count], rax
count 可能只加了 1 而不是 2。这叫竞态条件(Race Condition)。在汇编中,会使用 LOCK 前缀指令来解决:
五、 操作系统是如何创建它们的?
在 Linux 环境下,通过系统调用(Syscall)创建:
* fork:创建一个全新的进程。它会复制父进程的所有内存,但两者的内存地址之后不再相关。
* clone(pthread_create 的底层):创建一个线程。它指向父进程的内存地址。
总结
- 进程是“容器”,负责找系统要钱、要地皮(内存、文件)。
- 线程是“执行者”,负责拿着 CPU 给的指令在进程的地皮上干活。
- 汇编程序员直接面对的是线程的寄存器和栈。