第二十九章:分身有术
筑基中期涉及内核源码:
一
林小源在内核中遇到了一个他从未见过的现象。
一个进程,有两个执行流。它们共享同一个地址空间、同一个文件描述符表、同一个 。但它们有不同的栈、不同的寄存器状态、不同的 。
他走近一看,两个长得几乎一模一样的"人"站在同一间屋子里——屋子的墙壁、地板、天花板都是共享的,但两个人各自拿着不同的书,坐在不同的椅子上。
"你们是……同一个人?"林小源问。
左边那个抬起头。"不完全是。"他说,"我叫主线程,PID 100,tgid 100。"
右边那个也抬起头。"我是工作线程,PID 101,tgid 100。"
"tgid 相同?"
"对。"主线程说,"在用户态看来,我们是同一个进程。 都返回 100。但在内核看来,我们是两个独立的 。"
/*
* clone() 的关键标志:
* CLONE_VM — 共享地址空间(mm_struct)
* CLONE_FS — 共享文件系统信息
* CLONE_FILES — 共享文件描述符表
* CLONE_SIGHAND — 共享信号处理器
* CLONE_THREAD — 同一线程组(同一线程)
*
* fork() = clone(SIGCHLD, 0)
* 所有资源都复制
*
* 线程 = clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD, stack)
* 大部分资源共享,只有栈独立
*/
#define CLONE_VM 0x00000100
#define CLONE_FS 0x00000200
#define CLONE_FILES 0x00000400
#define CLONE_SIGHAND 0x00000800
#define CLONE_THREAD 0x00010000
struct clone_args {
unsigned long flags;
const char *description;
};
struct clone_args clones[] = {
{ 0, "fork() — 完全复制" },
{ CLONE_VM, "共享地址空间" },
{ CLONE_VM | CLONE_FILES, "共享地址空间 + 文件描述符" },
{ CLONE_VM | CLONE_FS | CLONE_FILES |
CLONE_SIGHAND | CLONE_THREAD, "线程 — 几乎全部共享" },
};
int nr = sizeof(clones) / sizeof(clones[0]);
printf("=== clone() 标志与资源共享 ===\n\n");
for (int i = 0; i < nr; i++) {
printf("--- %s ---\n", clones[i].description);
printf(" CLONE_VM: %s\n",
(clones[i].flags & CLONE_VM) ? "共享" : "复制");
printf(" CLONE_FS: %s\n",
(clones[i].flags & CLONE_FS) ? "共享" : "复制");
printf(" CLONE_FILES: %s\n",
(clones[i].flags & CLONE_FILES) ? "共享" : "复制");
printf(" CLONE_SIGHAND: %s\n",
(clones[i].flags & CLONE_SIGHAND) ? "共享" : "复制");
printf(" CLONE_THREAD: %s\n\n",
(clones[i].flags & CLONE_THREAD) ? "是" : "否");
}
printf("--- 进程 vs 线程 ---\n");
printf("进程: 独立的地址空间、文件描述符、信号处理器\n");
printf("线程: 共享地址空间、文件描述符、信号处理器\n");
printf(" 只有栈和寄存器状态独立\n\n");
printf("线程的优点:\n");
printf(" - 创建快(不需要复制页表)\n");
printf(" - 切换快(不需要切换页表)\n");
printf(" - 通信方便(共享内存)\n\n");
printf("线程的缺点:\n");
printf(" - 一个线程崩溃,所有线程都崩溃\n");
printf(" - 需要同步机制(锁、信号量)\n");
printf(" - 一个线程阻塞,所有线程都阻塞(内核线程)\n");#include <stdio.h>
#include <string.h>
/*
* clone() 的关键标志:
* CLONE_VM — 共享地址空间(mm_struct)
* CLONE_FS — 共享文件系统信息
* CLONE_FILES — 共享文件描述符表
* CLONE_SIGHAND — 共享信号处理器
* CLONE_THREAD — 同一线程组(同一线程)
*
* fork() = clone(SIGCHLD, 0)
* 所有资源都复制
*
* 线程 = clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD, stack)
* 大部分资源共享,只有栈独立
*/
#define CLONE_VM 0x00000100
#define CLONE_FS 0x00000200
#define CLONE_FILES 0x00000400
#define CLONE_SIGHAND 0x00000800
#define CLONE_THREAD 0x00010000
struct clone_args {
unsigned long flags;
const char *description;
};
int main() {
struct clone_args clones[] = {
{ 0, "fork() — 完全复制" },
{ CLONE_VM, "共享地址空间" },
{ CLONE_VM | CLONE_FILES, "共享地址空间 + 文件描述符" },
{ CLONE_VM | CLONE_FS | CLONE_FILES |
CLONE_SIGHAND | CLONE_THREAD, "线程 — 几乎全部共享" },
};
int nr = sizeof(clones) / sizeof(clones[0]);
printf("=== clone() 标志与资源共享 ===\n\n");
for (int i = 0; i < nr; i++) {
printf("--- %s ---\n", clones[i].description);
printf(" CLONE_VM: %s\n",
(clones[i].flags & CLONE_VM) ? "共享" : "复制");
printf(" CLONE_FS: %s\n",
(clones[i].flags & CLONE_FS) ? "共享" : "复制");
printf(" CLONE_FILES: %s\n",
(clones[i].flags & CLONE_FILES) ? "共享" : "复制");
printf(" CLONE_SIGHAND: %s\n",
(clones[i].flags & CLONE_SIGHAND) ? "共享" : "复制");
printf(" CLONE_THREAD: %s\n\n",
(clones[i].flags & CLONE_THREAD) ? "是" : "否");
}
printf("--- 进程 vs 线程 ---\n");
printf("进程: 独立的地址空间、文件描述符、信号处理器\n");
printf("线程: 共享地址空间、文件描述符、信号处理器\n");
printf(" 只有栈和寄存器状态独立\n\n");
printf("线程的优点:\n");
printf(" - 创建快(不需要复制页表)\n");
printf(" - 切换快(不需要切换页表)\n");
printf(" - 通信方便(共享内存)\n\n");
printf("线程的缺点:\n");
printf(" - 一个线程崩溃,所有线程都崩溃\n");
printf(" - 需要同步机制(锁、信号量)\n");
printf(" - 一个线程阻塞,所有线程都阻塞(内核线程)\n");
return 0;
}林小源盯着那两个 ,试图理解它们之间的关系。它们的 指针指向同一个地址——同一间屋子。它们的 指针也相同——同一张文件描述符表。但它们的栈不同,寄存器状态不同。
"那你们怎么区分彼此?"林小源问。
"用户态用 。"工作线程说,"它返回的是 ,不是 。但大多数程序不调用 ——它们只用 ,所以它们以为我们是同一个人。"
二
林小源在研究线程的过程中,发现了一个让他困惑的事实。
在 Linux 内核中,线程和进程没有本质区别。它们都是 。内核不区分"进程"和"线程"——它只看 的标志位。
"那 和 的区别是什么?"林小源问主线程。
" 是 clone(SIGCHLD, 0)。"主线程说,"所有资源都复制——新的地址空间、新的文件描述符表、新的信号处理器。而 可以通过标志位精确控制哪些资源共享。"
"所以线程就是 带了 CLONE_VM | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD?"
"差不多。"工作线程接过话,"但你也可以让两个进程共享文件描述符但不共享地址空间,或者共享信号处理器但不共享文件描述符。 的标志位是自由组合的。"
/*
* 在内核中,线程和进程都是 task_struct。
* 区别仅在于 clone() 的标志位。
*
* task_struct 中的关键字段:
* mm — 地址空间(CLONE_VM 控制是否共享)
* files — 文件描述符表(CLONE_FILES 控制是否共享)
* sighand — 信号处理器(CLONE_SIGHAND 控制是否共享)
* tgid — 线程组 ID(等于主线程的 PID)
* pid — 线程自己的 PID
*
* 同一进程中的线程:
* tgid 相同(等于进程的 PID)
* pid 不同(每个线程有唯一的 PID)
*/
struct task_struct {
int pid; /* 线程自己的 PID */
int tgid; /* 线程组 ID(进程 PID) */
void *mm; /* 地址空间 */
void *files; /* 文件描述符表 */
void *sighand; /* 信号处理器 */
char comm[16];
};
printf("=== 线程的内核视角 ===\n\n");
/* 一个进程有两个线程 */
struct task_struct main_thread = {
.pid = 100, .tgid = 100,
.mm = (void *)0x1000, .files = (void *)0x2000,
.sighand = (void *)0x3000, .comm = "my_program"
};
struct task_struct worker_thread = {
.pid = 101, .tgid = 100,
.mm = (void *)0x1000, .files = (void *)0x2000,
.sighand = (void *)0x3000, .comm = "my_program"
};
printf("主线程:\n");
printf(" pid=%d, tgid=%d, comm=%s\n",
main_thread.pid, main_thread.tgid, main_thread.comm);
printf(" mm=%p, files=%p, sighand=%p\n\n",
main_thread.mm, main_thread.files, main_thread.sighand);
printf("工作线程:\n");
printf(" pid=%d, tgid=%d, comm=%s\n",
worker_thread.pid, worker_thread.tgid, worker_thread.comm);
printf(" mm=%p, files=%p, sighand=%p\n\n",
worker_thread.mm, worker_thread.files, worker_thread.sighand);
printf("--- 关键观察 ---\n");
printf("tgid 相同: %s (都是 %d)\n",
main_thread.tgid == worker_thread.tgid ? "是" : "否",
main_thread.tgid);
printf("pid 不同: %d vs %d\n",
main_thread.pid, worker_thread.pid);
printf("mm 相同: %s (共享地址空间)\n",
main_thread.mm == worker_thread.mm ? "是" : "否");
printf("files 相同: %s (共享文件描述符)\n",
main_thread.files == worker_thread.files ? "是" : "否");
printf("sighand 相同: %s (共享信号处理器)\n",
main_thread.sighand == worker_thread.sighand ? "是" : "否");
printf("\n--- 用户态视角 ---\n");
printf("用户态看到的\"进程 ID\"是 tgid,不是 pid\n");
printf("getpid() 返回 tgid\n");
printf("gettid() 返回 pid\n");
printf("kill() 按 tgid 发送信号(发给整个线程组)\n");#include <stdio.h>
/*
* 在内核中,线程和进程都是 task_struct。
* 区别仅在于 clone() 的标志位。
*
* task_struct 中的关键字段:
* mm — 地址空间(CLONE_VM 控制是否共享)
* files — 文件描述符表(CLONE_FILES 控制是否共享)
* sighand — 信号处理器(CLONE_SIGHAND 控制是否共享)
* tgid — 线程组 ID(等于主线程的 PID)
* pid — 线程自己的 PID
*
* 同一进程中的线程:
* tgid 相同(等于进程的 PID)
* pid 不同(每个线程有唯一的 PID)
*/
struct task_struct {
int pid; /* 线程自己的 PID */
int tgid; /* 线程组 ID(进程 PID) */
void *mm; /* 地址空间 */
void *files; /* 文件描述符表 */
void *sighand; /* 信号处理器 */
char comm[16];
};
int main() {
printf("=== 线程的内核视角 ===\n\n");
/* 一个进程有两个线程 */
struct task_struct main_thread = {
.pid = 100, .tgid = 100,
.mm = (void *)0x1000, .files = (void *)0x2000,
.sighand = (void *)0x3000, .comm = "my_program"
};
struct task_struct worker_thread = {
.pid = 101, .tgid = 100,
.mm = (void *)0x1000, .files = (void *)0x2000,
.sighand = (void *)0x3000, .comm = "my_program"
};
printf("主线程:\n");
printf(" pid=%d, tgid=%d, comm=%s\n",
main_thread.pid, main_thread.tgid, main_thread.comm);
printf(" mm=%p, files=%p, sighand=%p\n\n",
main_thread.mm, main_thread.files, main_thread.sighand);
printf("工作线程:\n");
printf(" pid=%d, tgid=%d, comm=%s\n",
worker_thread.pid, worker_thread.tgid, worker_thread.comm);
printf(" mm=%p, files=%p, sighand=%p\n\n",
worker_thread.mm, worker_thread.files, worker_thread.sighand);
printf("--- 关键观察 ---\n");
printf("tgid 相同: %s (都是 %d)\n",
main_thread.tgid == worker_thread.tgid ? "是" : "否",
main_thread.tgid);
printf("pid 不同: %d vs %d\n",
main_thread.pid, worker_thread.pid);
printf("mm 相同: %s (共享地址空间)\n",
main_thread.mm == worker_thread.mm ? "是" : "否");
printf("files 相同: %s (共享文件描述符)\n",
main_thread.files == worker_thread.files ? "是" : "否");
printf("sighand 相同: %s (共享信号处理器)\n",
main_thread.sighand == worker_thread.sighand ? "是" : "否");
printf("\n--- 用户态视角 ---\n");
printf("用户态看到的\"进程 ID\"是 tgid,不是 pid\n");
printf("getpid() 返回 tgid\n");
printf("gettid() 返回 pid\n");
printf("kill() 按 tgid 发送信号(发给整个线程组)\n");
return 0;
}归根结底,用户态说的"PID",在内核眼里其实是"tgid"。
林小源盯着那些指针,脑中浮现出一个哲学问题:如果两个线程共享同一个地址空间,它们是"同一个人"的不同想法,还是"两个不同的人"恰好住在一起?
"你觉得呢?"他问工作线程。
工作线程笑了笑。"我是从 诞生的。我有自己的栈,自己的寄存器,自己的执行流。但我和主线程共享同一间屋子。你说我是谁?"
林小源答不上来。
三
林小源在研究线程的过程中,想到了一个危险的问题。
"如果你们两个同时修改同一个变量呢?"他问主线程和工作线程。
主线程的脸色变了。
"那就出事了。"他说,语气变得沉重。"我读到 x 的值是 5,工作线程也读到 5。我把 x 改成 6,他把 x 改成 7。最终 x 是 7——但我的修改被丢失了。"
"竞态条件。"工作线程补充道,"共享地址空间是一把双刃剑。我们能直接通信,不需要管道、不需要复制——但我们也会互相踩踏。"
"那怎么办?"
"锁。"主线程说,"自旋锁、互斥锁、信号量、RCU——每一种都有自己的适用场景。"
林小源在内核中确实看到了各种同步机制。自旋锁适合短临界区——持有者忙等,不睡眠。互斥锁适合长临界区——等待者可以睡眠,让出 CPU。信号量适合计数同步。RCU 适合读多写少的场景——读无锁,写延迟释放。
/*
* 线程同步机制:
* 自旋锁 (spinlock) — 忙等,适合短临界区
* 互斥锁 (mutex) — 睡眠等待,适合长临界区
* 信号量 (semaphore) — 计数同步
* RCU — 读无锁,写延迟释放
*/
printf("=== 线程同步机制 ===\n\n");
printf("--- 自旋锁 (spinlock) ---\n");
printf(" 忙等:不断检查锁状态\n");
printf(" 适合:临界区很短(< 1ms)\n");
printf(" 不适合:临界区长(浪费 CPU)\n\n");
printf("--- 互斥锁 (mutex) ---\n");
printf(" 睡眠等待:获取不到就让出 CPU\n");
printf(" 适合:临界区较长\n");
printf(" 不适合:中断上下文(不能睡眠)\n\n");
printf("--- 信号量 (semaphore) ---\n");
printf(" 计数器:允许多个进程同时进入\n");
printf(" 适合:资源池(如数据库连接池)\n\n");
printf("--- RCU (Read-Copy-Update) ---\n");
printf(" 读无锁:读者不需要任何同步\n");
printf(" 写延迟:写者先复制,再更新指针\n");
printf(" 适合:读多写少(如路由表)\n\n");
printf("--- 选择指南 ---\n");
printf("中断上下文 + 短临界区 → 自旋锁\n");
printf("进程上下文 + 长临界区 → 互斥锁\n");
printf("资源池 + 计数 → 信号量\n");
printf("读多写少 → RCU\n");#include <stdio.h>
/*
* 线程同步机制:
* 自旋锁 (spinlock) — 忙等,适合短临界区
* 互斥锁 (mutex) — 睡眠等待,适合长临界区
* 信号量 (semaphore) — 计数同步
* RCU — 读无锁,写延迟释放
*/
int main() {
printf("=== 线程同步机制 ===\n\n");
printf("--- 自旋锁 (spinlock) ---\n");
printf(" 忙等:不断检查锁状态\n");
printf(" 适合:临界区很短(< 1ms)\n");
printf(" 不适合:临界区长(浪费 CPU)\n\n");
printf("--- 互斥锁 (mutex) ---\n");
printf(" 睡眠等待:获取不到就让出 CPU\n");
printf(" 适合:临界区较长\n");
printf(" 不适合:中断上下文(不能睡眠)\n\n");
printf("--- 信号量 (semaphore) ---\n");
printf(" 计数器:允许多个进程同时进入\n");
printf(" 适合:资源池(如数据库连接池)\n\n");
printf("--- RCU (Read-Copy-Update) ---\n");
printf(" 读无锁:读者不需要任何同步\n");
printf(" 写延迟:写者先复制,再更新指针\n");
printf(" 适合:读多写少(如路由表)\n\n");
printf("--- 选择指南 ---\n");
printf("中断上下文 + 短临界区 → 自旋锁\n");
printf("进程上下文 + 长临界区 → 互斥锁\n");
printf("资源池 + 计数 → 信号量\n");
printf("读多写少 → RCU\n");
return 0;
}"选择错误的锁,代价是什么?"林小源问。
"死锁。"主线程和工作线程同时说出了这个词。
主线程继续道:"如果我持有锁 A 等锁 B,工作线程持有锁 B 等锁 A——我们两个永远等下去。整个系统卡住。"
"所以内核有锁序规则。"工作线程说,"所有代码必须按相同的顺序获取锁。违反锁序, 会报警。"
林小源感到了一阵寒意。线程的世界不是他想象的那么简单——共享地址空间带来了通信的便利,但也带来了竞态、死锁、优先级反转等一系列问题。
线程的世界,充满了"竞争"和"协作"。
道藏笔记
内核启示
线程是通过 创建的"轻量级进程"。
和 走的是同一条内核路径 ,区别全在标志位上: 让父子共享地址空间, 共享文件描述符表, 共享信号处理器, 把它们划进同一个线程组。在内核眼里,线程和进程都是 ,内核根本不区分——它只看 传了什么标志。
用户态的视角就不一样了。 返回的是 (线程组 ID,也就是进程 PID), 才返回线程自己的 。 按 发信号,一发就是整个线程组收到。
线程共享地址空间带来了便利,也带来了竞态问题。好在内核有一套同步机制:自旋锁适合短临界区(忙等不睡觉),互斥锁适合长临界区(等不到就让出 CPU),信号量管计数同步,RCU 在读多写少的场景下让读者完全无锁。
线程是进程的"分身"——共享肉身,各有灵魂。
分身之试
比 fork 更细粒度、能按 flags 选择共享资源的分身接口是什么?