Linux 线程
线程概念
1. 线程与进程的区别
进程:拥有独立的地址空间和PCB(进程控制块),是操作系统资源分配的基本单位。
线程:没有独立的地址空间,而是共享其所属进程的地址空间,但每个线程都有自己的PCB,是CPU调度的基本单位。
从控制角度时说。控制回路有3条路,反馈,前馈,前向,输入以及输出。
整个控制回路系统就是一个进程,PCB管理这个系统的整体资源(内存、IO设备、定时器等),
控制回路中的三条路径(反馈、前馈、前向)是三个线程,它们:
共享同一个控制系统的资源(传感器数据、执行器、共享变量)
但并行执行各自的计算任务
每个线程有自己的TCB来记录执行状态
PCB管理整个控制系统的"家当",TCB管理每个控制路径的"执行状态""
想象一个自动化工厂:
整个工厂 = 进程(有完整的生产系统)
工厂档案(PCB) = 记录工厂的整体状态、设备清单、原料库存
三条生产线 = 三个线程:
反馈控制线程(监控产品质量)
前馈控制线程(预测原料需求)
前向控制线程(执行生产指令)
工人工作卡(TCB) = 记录每条生产线当前的工作状态
工作流程:
三条生产线(线程)在同一个工厂(进程)内并行工作
它们共享工厂的传感器数据、控制参数、执行机构
调度器通过查看每条生产线的工作卡(TCB)来决定CPU时间分配
工厂档案(PCB)记录整个系统的资源使用情况
场景1:单进程模型
+-------------------------------+
|          Process A            |
|  +--------------------------+ |
|  |        Address Space     | |
|  |  +--------------------+  | |
|  |  |       Code         |  | |
|  |  +--------------------+  | |
|  |  |       Data         |  | |
|  |  +--------------------+  | |
|  |  |        Heap        |  | |
|  |  +--------------------+  | |
|  |  |       Stack        |  | |
|  |  +--------------------+  | |
|  +--------------------------+ |
|  +--------------------------+ |
|  |           PCB           | |
|  |   - PID: 1000          | |
|  |   - State: Running     | |
|  |   - ...                | |
|  +--------------------------+ |
+-------------------------------+场景2:多线程模型(同一进程内)
+---------------------------------------------------+
|                 Process A                         |
|   +---------------------------------------------+ |
|   |              Shared Address Space           | |
|   |  +-------------------+ +-----------------+  | |
|   |  |      Code         | |      Code       |  | |
|   |  +-------------------+ +-----------------+  | |
|   |  |      Data         | |      Data       |  | |
|   |  +-------------------+ +-----------------+  | |
|   |  |       Heap        | |       Heap      |  | |
|   |  +-------------------+ +-----------------+  | |
|   |  | Thread 1's Stack  | | Thread 2's Stack|  | |
|   |  +-------------------+ +-----------------+  | |
|   +---------------------------------------------+ |
|   +-------------------+   +-------------------+   |
|   |      PCB 1        |   |      PCB 2        |   |
|   |  - PID: 1000      |   |  - PID: 1001      |   |
|   |  - TGID: 1000     |   |  - TGID: 1000     |   |
|   |  - State: Running |   |  - State: Ready   |   |
|   +-------------------+   +-------------------+   |
+---------------------------------------------------+三级映射详细解析
映射流程(修正版)
线程PCB → 页目录(PD) → 页表(PT) → 物理页面(PP) → 内存单元各级结构说明
1. 页目录 (Page Directory)
位置:位于进程的PCB中
大小:4KB,包含1024个表项
作用:每个表项指向一个页表
特点:同一进程的所有线程共享同一个页目录
2. 页表 (Page Table)
大小:4KB,包含1024个表项
作用:每个表项指向一个物理页面
映射范围:一个页表映射 1024 × 4KB = 4MB 地址空间
3. 物理页面 (Physical Page)
大小:4KB
作用:实际的物理内存块
包含:1024个内存单元(每个单元1字节)
地址转换过程
虚拟地址 = [10位页目录索引] + [10位页表索引] + [12位页内偏移]1. 通过页目录索引找到页表
2. 通过页表索引找到物理页面  
3. 通过页内偏移找到具体内存单元线程共享原理详解
关键机制
// 线程创建时共享地址空间的关键
clone(..., CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, ...);CLONE_VM 标志:让新线程与父线程共享同一个页目录和地址空间映射。
线程共享示意图
进程A
├── 页目录 (所有线程共享)
├── 线程1-PCB → 共享页目录
├── 线程2-PCB → 共享页目录  
└── 线程3-PCB → 共享页目录↓
相同的页表映射 → 相同的物理页面与进程的对比
进程A:独立页目录A → 页表集A → 物理页面集X
进程B:独立页目录B → 页表集B → 物理页面集Y线程A1、A2、A3:共享页目录A → 共享页表集A → 共享物理页面集X实际内存映射流程
完整路径
线程执行指令↓
CPU遇到虚拟地址↓
查线程PCB中的CR3寄存器 → 找到页目录物理地址↓  
通过虚拟地址前10位索引页目录 → 找到页表↓
通过虚拟地址中间10位索引页表 → 找到物理页面↓  
物理页面基址 + 页内偏移(12位) = 物理地址↓
访问内存单元关键点说明
CR3寄存器:每个线程的PCB中都保存着CR3值,指向页目录的物理地址
共享原理:同一进程的所有线程,其CR3指向同一个页目录
TLB加速:频繁的地址转换通过TLB(快表)缓存,提高性能
为什么线程比进程快?
1. 创建开销小
线程:只需创建PCB和栈,共享现有页目录
进程:需要创建完整的页目录、页表结构
2. 切换开销小
线程切换:主要保存寄存器状态,地址空间不变
进程切换:需要切换整个页目录(刷新CR3和TLB)
3. 通信成本低
线程:直接通过共享内存通信
进程:需要IPC机制,涉及内核拷贝
个人理解
我这么理解。cpu和内核以及内存资,都是计算机资源,我们要充分利用。 但是显示当中内存难以分配以及计算效率不高,甚至会引起信息传输不安全。所以发展出了mmu以及pcb,通过创建一个虚拟内存空间,将虚拟的内存空间与现实空间进行映射,创建映射表,这样充分利用资源,同时还将信息进行隔离保证安全。 除上述之外,这个操作还不够细腻。一个进程分配一个pcb,但是pcb的内存也不小,为了充分利用资源也要对pcb的资源进一步规划以及分配,针对pcb的内存也相当于使用一个表格进行管理。
从物理内存到虚拟内存
原始状态:程序直接操作物理内存↓
问题:内存碎片、安全风险、效率低下↓
解决方案:引入MMU + 虚拟地址空间↓
效果:每个进程有独立的虚拟世界,通过映射表访问真实物理内存PCB的精细化管理的演进
第一阶段:粗粒度 - 仅进程
一个程序 = 一个进程 = 一个PCB↓
问题:一个程序内部无法并行,创建进程开销大↓
解决方案:引入线程概念第二阶段:细粒度 - 进程+线程
// Linux中的实现:task_struct 既可以是进程也可以是线程
struct task_struct {pid_t pid;          // 进程IDpid_t tgid;         // 线程组ID(进程ID)struct mm_struct *mm; // 内存管理结构(线程间共享)// ... 其他字段
};PCB管理的"表格化"理解
您说的"针对PCB的内存也相当于使用一个表格进行管理"非常准确
操作系统内核维护:
┌─────────────────┐
│  进程/线程表    │ ← 这就是管理PCB的"表格"
│  - task_struct *│
│  - 状态         │
│  - 调度信息     │
│  - ...         │
└─────────────────┘↓ 指向各个PCB
┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│  线程1-PCB  │  │  线程2-PCB  │  │  进程B-PCB  │
│  - pid      │  │  - pid      │  │  - pid      │
│  - tgid     │  │  - tgid     │  │  - tgid     │  
│  - mm       │  │  - mm       │  │  - mm       │
└─────────────┘  └─────────────┘  └─────────────┘共享同一mm       共享同一mm        独立mm完整的技术栈视图
资源管理的层次化
最底层:物理资源CPU核心、物理内存条、硬件设备↓
第一层抽象:MMU虚拟化  └── 虚拟地址空间 + 页表映射↓
第二层抽象:进程管理└── PCB + 进程调度↓  
第三层抽象:线程管理└── 轻量级PCB + 线程调度↓
最高层:应用程序└── 看到的是统一的编程接口创建线程:pthread_create()
1. 函数原型
int pthread_create(pthread_t *thread,               // 传出参数:保存新线程IDconst pthread_attr_t *attr,      // 线程属性(通常 NULL)void *(*start_routine)(void *),  // 线程要执行的函数(入口)void *arg                        // 传给该函数的参数
);2. 参数详解
| 参数 | 说明 | 
|---|---|
thread | 传出参数,函数成功后会把新线程的 ID 写入这里。类型是 pthread_t*。 | 
attr | 线程属性(如栈大小、是否分离等)。初学者传 NULL 表示使用默认属性。 | 
start_routine | 回调函数,新线程启动后执行这个函数。必须是 void* func(void*) 形式。 | 
arg | 传给回调函数的参数,类型是 void*,可以传任意类型的指针(如 int*, struct* 等)。 | 
3. 返回值
- 成功:返回 
0 - 失败:直接返回错误码(如 
EAGAIN,EINVAL),不是通过errno!- 这是和传统系统调用(如 
open,fork)的重要区别! 
 - 这是和传统系统调用(如 
 
必须注意:
回调函数签名必须严格匹配:
void* my_func(void* arg) { ... }不能是
void my_func()或int my_func(void*)!线程 ID 类型是
pthread_t,在 Linux 下通常是unsigned long,但不要假设,直接用%lu打印时要强转:printf("Thread ID: %lu\n", (unsigned long)tid);
示例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void* my_thread_func(void* arg) {printf("Child thread running! ID = %lu\n", (unsigned long)pthread_self());sleep(1);return NULL;
}int main() {pthread_t tid;printf("Main thread ID = %lu\n", (unsigned long)pthread_self());int ret = pthread_create(&tid, NULL, my_thread_func, NULL);if (ret != 0) {fprintf(stderr, "Failed to create thread: %d\n", ret);return 1;}sleep(2); // 等待子线程执行完(临时方案,后续用 pthread_join)return 0;
}获取线程 ID:pthread_self()
1. 函数原型
#include <pthread.h>
pthread_t pthread_self(void);2. 特点
- 总是成功:不会失败,也不返回错误码。
 - 返回当前线程的 ID:就像 
getpid()返回当前进程 ID 一样。 - 线程 ID 是进程内部的标识:不同进程中的线程 ID 可能相同,但它们互不影响。
 
3. 线程 ID vs LWP(轻量级进程号)
| 项目 | 线程 ID (pthread_self()) | LWP(ps -Lf 中看到的) | 
|---|---|---|
| 作用 | 进程内部标识线程 | 内核调度单位(CPU 时间片依据) | 
| 类型 | pthread_t(在 Linux 中通常是 unsigned long) | 整数 PID(其实是内核线程的 PID) | 
| 是否相同? | ❌ 不同!不要混淆! | 
✅ 打印建议:在 Linux 下,
pthread_t本质是unsigned long,所以用%lu打印:
printf("Thread ID = %lu\n", (unsigned long)pthread_self());4. 示例代码
#include <stdio.h>
#include <pthread.h>int main() {printf("Main thread ID = %lu\n", (unsigned long)pthread_self());return 0;
}线程中的错误处理:为什么不能用 perror?
1. 问题背景:errno 在多线程中不安全!
- 在传统单线程程序中,系统调用失败时会设置全局变量 
errno,然后你可以用perror()打印错误。 - 但在多线程环境中,多个线程共享同一个 
errno(虽然现代 glibc 已将其改为线程局部存储,但为了代码可移植性和明确性,仍建议避免依赖errno)。 - 更重要的是:
pthread_create等线程函数不会设置errno! 
✅ 关键事实:
pthread_create() 失败时直接返回错误码(如 EAGAIN, EINVAL),不是通过 errno!errno是输出错误原因,但是pthread_create是返回错误码。
怎么解决报错处理?
不碰 errno,只看返回值;用 strerror_r 解析信息;根据错误码(如 EAGAIN、EINVAL)针对性处理,确保线程安全和问题可定位。
小测试:
循环创建 5 个线程,每个打印自己的序号
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>// 子线程函数
void* tfn(void* arg) {// 将 void* 转回整数(注意 long 中转)int idx = (int)(long)arg;// 打印:序号从1开始,所以 idx+1printf("I'm %dth thread: pid=%d, tid=%lu\n",idx + 1,getpid(),(unsigned long)pthread_self());// 模拟不同执行时间(让输出更有序)sleep(idx + 1);return NULL; // 必须返回!
}int main() {const int N = 5;pthread_t tids[N];// 创建 N 个线程for (int i = 0; i < N; i++) {int ret = pthread_create(&tids[i], NULL, tfn, (void*)(long)i);if (ret != 0) {fprintf(stderr, "Create thread %d failed: %s\n", i, strerror(ret));exit(EXIT_FAILURE);}}// 主线程等待(临时方案:usleep)usleep(100000); // 100ms,确保子线程有时间输出printf("main thread: pid=%d, tid=%lu\n",getpid(),(unsigned long)pthread_self());return 0;
}erro:循环变量传地址导致线程参数错乱
void* tfn(void* arg) {int *p = (int*)arg;printf("Thread %d\n", *p); // 解引用指针return NULL;
}int main() {pthread_t tids[5];for (int i = 0; i < 5; i++) {pthread_create(&tids[i], NULL, tfn, &i); // ⚠️ 传的是 &i!}sleep(1);return 0;
}输出可能为:
Thread 3
Thread 3
Thread 5
Thread 5
Thread 5根本原因:所有线程共享同一个地址 &i
内存图解析:
主线程栈帧:
+------------------+
| int i = ?        | ← 地址固定,比如 0x7fff1234
+------------------+循环过程:
i=0 → 创建线程0,传 &i(0x7fff1234)
i=1 → 创建线程1,传 &i(还是 0x7fff1234!)
i=2 → 创建线程2,传 &i(仍是 0x7fff1234)
...- 所有线程的 
arg都指向同一个地址&i。 - 主线程在 
for循环中不断执行i++。 - 子线程启动后,什么时候执行 
*p是不确定的(由调度器决定)。 - 当子线程终于执行到 
printf时,i可能已经变成 3、5 甚至循环结束了(i=5)! 
正确做法:传值,不传地址!
✅ 正确代码:
void* tfn(void* arg) {int num = (int)(long)arg; // 从 void* 还原整数printf("Thread %d\n", num);return NULL;
}int main() {pthread_t tids[5];for (int i = 0; i < 5; i++) {pthread_create(&tids[i], NULL, tfn, (void*)(long)(i + 1)); // 传值!}sleep(1);return 0;
}✅ 输出:
Thread 1
Thread 2
Thread 3
Thread 4
Thread 5为什么这样安全?
- 每个线程收到的是 
i的一个独立副本(数值被编码进指针值中)。 - 主线程后续修改 
i,完全不影响子线程拿到的值。 - 没有共享内存,没有竞争条件,线程安全!
 
关于 (void*)(long)i 的深度解释
问题:为什么不能直接 (void*)i?
在 64 位系统:
int i = 3;→ 4 字节:0x00000003(void*)i→ 8 字节指针:0x0000000000000003- 看似没问题,但编译器会警告:
warning: cast to pointer from integer of different size [-Wint-to-pointer-cast] 
为什么用 long 中转?
- 在 Linux 64 位系统,
long是 8 字节,和指针同宽。 (void*)(long)i先把int扩展为 8 字节long,再转指针,消除宽度不匹配警告。- 更标准的做法是用 
intptr_t(定义在<stdint.h>):#include <stdint.h> pthread_create(..., (void*)(intptr_t)i); int num = (int)(intptr_t)arg; 
✅ 初学记住:用
(void*)(long)i传整数,用(int)(long)arg取回,即可安全又少警告。
扩展思考:如果必须传结构体怎么办?
typedef struct { int id; char name[20]; } Task;// 正确做法:每个线程分配独立内存
for (int i = 0; i < 5; i++) {Task *task = malloc(sizeof(Task));task->id = i + 1;strcpy(task->name, "worker");pthread_create(&tid, NULL, tfn, task); // 传堆地址
}// 子线程中:
void* tfn(void* arg) {Task *t = (Task*)arg;printf("Task %d\n", t->id);free(t); // 谁分配,谁释放!return NULL;
}