Linux线程控制:从用户态控制到内核级克隆全链路解析 上篇热文Linux线程从内存分页机制Page Table/TLB/Page Fault彻底读懂 Linux 线程本质目录前言Linux 独特的“轻量级进程”哲学1. POSIX线程库2. 创建线程2.1 函数原型与参数说明2.2 代码验证主线程与子线程在同一个进程中2.3 反汇编底层机制剖析3. 深入理解用户级线程 IDpthread_t与内核级 LWP 的区别3.1 核心概念对比3.2 线程栈的进程地址空间布局分布4. 经典踩坑与实战多线程竞态条件与 C 对象传参4.1 共享栈缓冲区的竞态条件4.2 【实战】向线程传递 C 自定义类对象5. 线程终止5.1 方式一从线程函数 return5.2 方式二线程调用 pthread_exit 终止自己5.3 方式三调用 pthread_cancel 异常取消线程6. 线程等待6.1 为什么需要线程等待6.2 函数原型实验正常 join 阻塞等待6.3 为什么 join 无法收集“线程异常退出”信号6.4 高级实战多线程派发与双向 Task 对象回收7. 分离线程7.1 函数原型7.2 Joinable 与分离状态的冲突实证前言Linux 独特的“轻量级进程”哲学在传统操作系统的定义中进程和线程被赋予了截然不同的实体。但在 Linux 系统中这种界限变得极其模糊。在 CPU 眼中只存在一个又一个的执行流而没有专门用来描述线程的“独立结构体”。Linux 巧妙地复用了进程的代码使用轻量级进程LWP, Light Weight Process实现了线程。本文将在 Linux 环境下进行的多线程编程实战、反汇编底层探究和竞态条件调试彻底打通 Linux 线程控制从创建、终止、等待再到分离的用户态与内核态全链路流程。1. POSIX线程库Linux 的内核并没有为线程提供专有的系统调用内核只有轻量级进程为了让应用层开发者能够使用符合 POSIX 标准的多线程编程规范Linux 采用了用户态的原生线程库NPTLNative POSIX Thread Library。使用该库时需注意以下规范命名约定与线程有关的函数构成了一个完整的系列绝大多数函数的名字都是以pthread_打头的。头文件必须引入pthread.h头文件。链接选项链接这些线程函数库时要使用编译器命令的-lpthread选项例如g test.cpp -lpthread。2. 创建线程2.1 函数原型与参数说明thread输出型参数返回线程 ID。attr设置线程属性传入NULL表示使用默认属性。start_routine一个函数指针子线程启动后要执行的回调函数。arg传递给回调函数的参数。返回值成功返回0失败返回错误码。与传统系统调用不同pthread出错时不会设置全局变量errno而是直接将错误码通过返回值返回。2.2 代码验证主线程与子线程在同一个进程中在 Linux 系统中主线程和子线程运行在同一个进程空间内。我们可以编写如下代码进行观察#include iostream #include threads.h #include unistd.h void *hello(void *args) { while(true) { std::cout 子线程, pid: getpid() std::endl; sleep(1); } } int main() { pthread_t tid; pthread_create(tid, nullptr, hello, (void*)new-thread); while(true) { std::cout 主线程, pid: getpid() std::endl; sleep(1); } return 0; }结果通过ps -aL命令可以查看此命令是Linux 中查看系统进程及其所有线程的常用命令可以观察到在相同pid的前提下lwp(light weight process轻量级进程)不同。也就是说操作系统和CPU调度的基本单位是线程轻量级进程而进程是承担分配系统资源的基本实体。2.3 反汇编底层机制剖析mainhello底层机制划分页表所映射的页框将代码资源合理分配给指定的线程执行其底层逻辑十分朴素本质上是让不同的线程执行不同的函数接口。因为各函数在编译链接阶段编译器就已经为它们在代码段分配了唯一、确定且互不重叠的虚拟地址区间。当我们将函数指针传递给pthread_create时内核线程在被调度时只需将 PC 寄存器指向对应的虚拟地址入口即可。3. 深入理解用户级线程 IDpthread_t与内核级 LWP 的区别在打印线程 ID 时我们会发现通过pthread_self()得到的pthread_t与通过ps -aL查看到的 LWP 截然不同。我们通过以下代码进行验证#include iostream #include stdio.h #include string #include unistd.h #include pthread.h void *threadrun1(void *args) { std::string threadname static_castconst char *(args); while (true) { sleep(1); std::cout threadname std::endl; } } void *threadrun2(void *args) { std::string threadname static_castconst char *(args); while (true) { sleep(1); std::cout threadname std::endl; } } int main() { pthread_t t1, t2; pthread_create(t1, nullptr, threadrun1, (void *)thread-1); pthread_create(t2, nullptr, threadrun2, (void *)thread-2); while (true) { printf(Main thread, thread1 id: %p, thread2 id: %p\n, t1, t2); sleep(1); } return 0; }运行输出结果Main thread, thread1 id: 0x72205b9ff6c0, thread2 id: 0x72205b1fe6c0 thread-1 thread-23.1 核心概念对比用户级线程 IDpthread_t我们通过pthread_self()得到的这个数如0x72205b9ff6c0实际上是pthread库给每个线程定义的进程内唯一标识。怎么理解这个 “ID” 呢这个 “ID” 纯粹是由pthread库在用户态维持的。由于每个进程都有自己独立的虚拟地址空间故此 “ID” 的作用域是进程级而非系统级内核并不认识这个地址。内核级线程 IDLWPLWP得到的是真正的、系统全局唯一的线程 ID。虽然pthread库是通过内核提供的系统调用例如clone来创建线程的且内核会为每个轻量级进程分配全局唯一的LWP来进行 CPU 调度但在用户态我们无法直接通过简单变量获取它需要通过syscall(SYS_gettid)等间接手段。两者的桥梁关系之前使用pthread_self得到的pthread_t实际上是一个指针地址即位于虚拟地址空间共享区mmap区域上的一个内存地址。通过这个地址用户态线程库可以瞬间找到关于这个线程的所有基本维护信息包括线程在库内部的线程控制块TCB、线程私有栈空间、寄存器上下文等属性。3.2 线程栈的进程地址空间布局分布在ps -aL得到的线程信息中有一个线程的 LWP 和进程 PID 相同这个线程就是主线程。主线程的栈在虚拟地址空间的传统栈区Stack上。主线程的栈随着函数调用动态向下生长。其他线程的栈全部存在于共享区堆栈之间即mmap区域。因为pthread库是一个动态链接库加载时映射在共享区。库在创建子线程时通过mmap在共享区内划拨出一块专属的、固定大小一般默认$8\text{MB}$的内存作为该子线程的私有栈。4. 经典踩坑与实战多线程竞态条件与 C 对象传参4.1 共享栈缓冲区的竞态条件我们来看一个经典的因“共享栈上局部变量”导致的线程命名混乱 Bug#include iostream #include cstdio #include string #include vector #include unistd.h #include pthread.h const int gsize 64; void *threadrun(void *args) { std::string name static_castconst char *(args); while(true) { printf(我是一个新线程: tid: 0x%lx, pid: %d, name : %s\n, pthread_self(), getpid(), name.c_str()); sleep(1); } return nullptr; } int main(int argc, char *argv[]) { if(argc ! 2) { std::cout argv[0] num std::endl; return 1; } int num std::stoi(argv[1]); std::vectorpthread_t tids; for(int i 0; i num; i) { // 创建多线程 pthread_t tid; char threadname[gsize]; snprintf(threadname, sizeof(threadname), thread-%d, i1); pthread_create(tid, nullptr, threadrun, (void *)threadname); tids.push_back(tid); } sleep(1); for(auto tid : tids) { printf(main for 创建新线程成功, new tid: %lu, main tip: %lu, pid: %d\n, tid, pthread_self(), getpid()); } // 主线程 while(true) { std::cout main thread running... std::endl; sleep(1); } return 0; }运行结果见下发现其线程名每次都不一样。原因剖析 因为代码中char threadname[gsize]是在主线程的循环栈帧中分配的属于被多线程共享的栈区域。当主线程快速运转进行循环并修改缓冲区时部分子线程尚未被 CPU 调度起来执行std::string name ...的读取拷贝。当它们调度起来时缓冲区的数据早已被修改。这属于典型的竞态条件Race Condition引发的线程安全问题。$ ./createThread 10 我是一个新线程: tid: 0x7bb57f7ff6c0, pid: 4049290, name : thread-3 我是一个新线程: tid: 0x7bb57effe6c0, pid: 4049290, name : thread-4 我是一个新线程: tid: 0x7bb57e7fd6c0, pid: 4049290, name : thread-4 我是一个新线程: tid: 0x7bb57dffc6c0, pid: 4049290, name : thread-6 我是一个新线程: tid: 0x7bb577fff6c0, pid: 4049290, name : thread-6 我是一个新线程: tid: 0x7bb57d7fb6c0, pid: 4049290, name : thread-7 我是一个新线程: tid: 0x7bb57cffa6c0, pid: 4049290, name : thread-8 我是一个新线程: tid: 0x7bb5777fe6c0, pid: 4049290, name : thread-9 我是一个新线程: tid: 0x7bb576ffd6c0, pid: 4049290, name : thread-9 我是一个新线程: tid: 0x7bb5767fc6c0, pid: 4049290, name : thread-10 我是一个新线程: tid: 0x7bb57effe6c0, pid: 4049290, name : thread-4 我是一个新线程: tid: 0x7bb57e7fd6c0, pid: 4049290, name : thread-4 我是一个新线程: tid: 0x7bb57f7ff6c0, pid: 4049290, name : thread-3 我是一个新线程: tid: 0x7bb57dffc6c0, pid: 4049290, name : thread-6 我是一个新线程: tid: 0x7bb577fff6c0, pid: 4049290, name : thread-6 我是一个新线程: tid: 0x7bb57d7fb6c0, pid: 4049290, name : thread-7 我是一个新线程: tid: 0x7bb57cffa6c0, pid: 4049290, name : thread-8 我是一个新线程: tid: 0x7bb5777fe6c0, pid: 4049290, name : thread-9 我是一个新线程: tid: 0x7bb576ffd6c0, pid: 4049290, name : thread-9 我是一个新线程: tid: 0x7bb5767fc6c0, pid: 4049290, name : thread-10 main thread running... 我是一个新线程: tid: 0x7bb57effe6c0, pid: 4049290, name : thread-4 我是一个新线程: tid: 0x7bb57dffc6c0, pid: 4049290, name : thread-6 我是一个新线程: tid: 0x7bb57d7fb6c0, pid: 4049290, name : thread-7 我是一个新线程: tid: 0x7bb57f7ff6c0, pid: 4049290, name : thread-3 我是一个新线程: tid: 0x7bb577fff6c0, pid: 4049290, name : thread-6 我是一个新线程: tid: 0x7bb57cffa6c0, pid: 4049290, name : thread-8 我是一个新线程: tid: 0x7bb57e7fd6c0, pid: 4049290, name : thread-4 我是一个新线程: tid: 0x7bb5777fe6c0, pid: 4049290, name : thread-9 我是一个新线程: tid: 0x7bb576ffd6c0, pid: 4049290, name : thread-9 我是一个新线程: tid: 0x7bb5767fc6c0, pid: 4049290, name : thread-10修改代码给每个线程创建一份堆空间#include iostream #include cstdio #include string #include vector #include unistd.h #include pthread.h const int gsize 64; void *threadrun(void *args) { std::string name static_castconst char *(args); delete [](char*)args; while(true) { printf(我是一个新线程: tid: 0x%lx, pid: %d, name : %s\n, pthread_self(), getpid(), name.c_str()); sleep(1); } return nullptr; } int main(int argc, char *argv[]) { if(argc ! 2) { std::cout argv[0] num std::endl; return 1; } int num std::stoi(argv[1]); std::vectorpthread_t tids; for(int i 0; i num; i) { // 创建多线程 pthread_t tid; char *threadname new char[gsize]; snprintf(threadname, gsize, thread-%d, i1); pthread_create(tid, nullptr, threadrun, (void *)threadname); tids.push_back(tid); } sleep(1); for(auto tid : tids) { printf(main for 创建新线程成功, new tid: %lu, main tip: %lu, pid: %d\n, tid, pthread_self(), getpid()); } // 主线程 while(true) { std::cout main thread running... std::endl; sleep(1); } return 0; }4.2 【实战】向线程传递 C 自定义类对象在线程创建时不仅仅可以传递整数、字符指针因为形参是void*我们还可以传递任意 C 中的自定义类对象。Tesk.hpp#pragma once #include iostream #include string class Task { public: Task(const std::string who, int x, int y):_x(x), _y(y), _who(who) {} Task() {} void operator()() { std::cout _who execute task: _x _y _x _y std::endl; } ~Task() {} private: int _x; int _y; std::string _who; };testThread.cpp#include iostream #include cstdio #include string #include vector #include unistd.h #include pthread.h #include Tesk.hpp const int gsize 64; void *threadrun(void *args) { Task *t static_castTask *(args); sleep(1); (*t)(); sleep(1); while(true) { sleep(1); } return nullptr; } int main(int argc, char *argv[]) { if(argc ! 2) { std::cout argv[0] num std::endl; return 1; } int num std::stoi(argv[1]); std::vectorpthread_t tids; for(int i 0; i num; i) { // 创建多线程 pthread_t tid; // char *threadname new char[gsize]; char threadname[gsize]; snprintf(threadname, gsize, thread-%d, i1); Task *t new Task(threadname, 10 i, 20 * i); pthread_create(tid, nullptr, threadrun, (void *)t); tids.push_back(tid); sleep(1); } sleep(10); for(auto tid : tids) { printf(main for 创建新线程成功, new tid: %lu, main tip: %lu, pid: %d\n, tid, pthread_self(), getpid()); } // 主线程 while(true) { std::cout main thread running... std::endl; sleep(1); } return 0; }结果实证$ ./createThread 5 thread-1 execute task: 10 0 10 thread-2 execute task: 11 20 31 thread-3 execute task: 12 40 52 thread-4 execute task: 13 60 73 thread-5 execute task: 14 80 94这强有力地说明通过void*强转应用层能够实现极其灵活的面向对象多线程任务派发。5. 线程终止如果需要只终止某个线程而不终止整个进程可以有三种方法5.1 方式一从线程函数return这是最常规的退出方式。注意这种方法对主线程main函数不适用从main函数return相当于调用了exit()会导致整个进程及内部所有子线程全部终止。#include iostream #include cstdio #include string #include vector #include unistd.h #include pthread.h const int gsize 64; void *threadrun(void *args) { std::string name static_castconst char *(args); int cnt 5; while (cnt) { printf(我是一个新线程: tid: 0x%lx, pid: %d, name : %s, cnt: %d\n, pthread_self(), getpid(), name.c_str(), cnt); cnt--; sleep(1); } return nullptr; } int main() { pthread_t tid; char threadname[gsize]; snprintf(threadname, gsize, thread-%d, 1); pthread_create(tid, nullptr, threadrun, (void *)threadname); while(true) pause(); return 0; }现象$ ./createThread 我是一个新线程: tid: 0x7eda675ff6c0, pid: 4061333, name : thread-1, cnt: 5 我是一个新线程: tid: 0x7eda675ff6c0, pid: 4061333, name : thread-1, cnt: 4 我是一个新线程: tid: 0x7eda675ff6c0, pid: 4061333, name : thread-1, cnt: 3 我是一个新线程: tid: 0x7eda675ff6c0, pid: 4061333, name : thread-1, cnt: 2 我是一个新线程: tid: 0x7eda675ff6c0, pid: 4061333, name : thread-1, cnt: 15.2 方式二线程调用pthread_exit终止自己void pthread_exit(void *value_ptr);核心警示在多线程中千万不能调用exit()exit的职责是终止当前进程。在多线程程序的任何一个线程中调用exit()都表示整个进程退出瞬间抹杀所有其他线程执行流。5.3 方式三调用pthread_cancel异常取消线程int pthread_cancel(pthread_t thread);返回值被别的线程调用pthread_cancel异常取消掉的线程其通过pthread_join拿到的退出码将被设置为常数PTHREAD_CANCELED即(void*)-1。终止综合测试代码const int gsize 64; void *threadrun(void *args) { std::string name static_castconst char *(args); int cnt 5; while (cnt) { printf(我是一个新线程: tid: 0x%lx, pid: %d, name : %s, cnt: %d\n, pthread_self(), getpid(), name.c_str(), cnt); cnt--; sleep(1); } pthread_exit((void*)100); } int main() { pthread_t tid; char threadname[gsize]; snprintf(threadname, gsize, thread-%d, 1); pthread_create(tid, nullptr, threadrun, (void *)threadname); sleep(7); int n pthread_cancel(tid); printf(cancel new thread done, n : %d\n, n); void *ret nullptr; pthread_join(tid, ret); printf(join %lx success, ret code: %lld\n, tid, (long long)ret); return 0; }结果$ ./createThread 我是一个新线程: tid: 0x7009501ff6c0, pid: 4071303, name : thread-1, cnt: 5 我是一个新线程: tid: 0x7009501ff6c0, pid: 4071303, name : thread-1, cnt: 4 我是一个新线程: tid: 0x7009501ff6c0, pid: 4071303, name : thread-1, cnt: 3 我是一个新线程: tid: 0x7009501ff6c0, pid: 4071303, name : thread-1, cnt: 2 我是一个新线程: tid: 0x7009501ff6c0, pid: 4071303, name : thread-1, cnt: 1 cancel new thread done, n : 0 join 7009501ff6c0 success, ret code: 100之后创建多线程推荐这样做代码#include iostream #include cstdio #include string #include vector #include unistd.h #include pthread.h const int gsize 64; void *threadrun(void *args) { int cnt 5; while(cnt--) { sleep(1); } return nullptr; } int main(int argc, char *argv[]) { if(argc ! 2) { std::cout argv[0] num std::endl; return 1; } int num std::stoi(argv[1]); std::vectorpthread_t tids; for(int i 0; i num; i) { // 创建多线程 pthread_t tid; char threadname[gsize]; snprintf(threadname, gsize, thread-%d, i1); pthread_create(tid, nullptr, threadrun, threadname); tids.push_back(tid); sleep(1); } for(auto tid: tids) { pthread_join(tid, nullptr); std::cout join success: tid std::endl; } return 0; }结果$ ./createThread 5 join success: 132617013819072 join success: 132617005426368 join success: 132616997033664 join success: 132616988640960 join success: 1326169802482566. 线程等待6.1 为什么需要线程等待已经退出的线程其系统内部控制块空间TCB及栈资源没有被完全释放仍然驻留在进程的地址空间内会造成类似于僵尸进程的内存泄漏。创建新的线程时系统不会主动复用刚才退出线程的地址空间。6.2 函数原型int pthread_join(pthread_t thread, void **value_ptr);thread目标线程 ID。value_ptr指向指针的指针用来接收子线程退出的返回值即return的值或pthread_exit的参数。实验正常 join 阻塞等待const int gsize 64; void *threadrun(void *args) { std::string name static_castconst char *(args); int cnt 5; while (cnt) { printf(我是一个新线程: tid: 0x%lx, pid: %d, name : %s, cnt: %d\n, pthread_self(), getpid(), name.c_str(), cnt); cnt--; sleep(1); // return nullptr; // pthread_exit(nullptr); } return (void*)10; // 将数字写到指针变量中 // return nullptr; // pthread_exit(nullptr); } int main() { pthread_t tid; char threadname[gsize]; snprintf(threadname, gsize, thread-%d, 1); pthread_create(tid, nullptr, threadrun, (void *)threadname); void *ret nullptr; pthread_join(tid, ret); printf(join %lx success, ret code: %lld\n, tid, (long long)ret); // while(true) // pause(); return 0; }结果$ ./createThread 我是一个新线程: tid: 0x72fe251ff6c0, pid: 4068480, name : thread-1, cnt: 5 我是一个新线程: tid: 0x72fe251ff6c0, pid: 4068480, name : thread-1, cnt: 4 我是一个新线程: tid: 0x72fe251ff6c0, pid: 4068480, name : thread-1, cnt: 3 我是一个新线程: tid: 0x72fe251ff6c0, pid: 4068480, name : thread-1, cnt: 2 我是一个新线程: tid: 0x72fe251ff6c0, pid: 4068480, name : thread-1, cnt: 1 join 72fe251ff6c0 success, ret code: 106.3 为什么 join 无法收集“线程异常退出”信号在进程等待中waitpid可以检测进程是否因异常信号如段错误退出。为什么pthread_join却完全没有相关的异常状态位原因解析因为线程是进程内的一个执行流。只要任何一个线程发生致命异常如除 0、越界操作系统发送的信号是针对整个进程的。信号会导致整个进程挂掉所有的线程也会在一瞬间覆灭。既然崩溃会引发整个进程退出那么在进程内进行join收集子线程异常也就失去了物理意义。所以pthread_join只关心正常退出如果不退出pthread_join会一直阻塞等待下去。6.4 高级实战多线程派发与双向 Task 对象回收我们可以让子线程不仅在启动时接收类对象参数在退出时还能通过join将在堆区计算完毕的类对象完整返回给主线程进行结果统计。Task.hpp 优化版#pragma once #include iostream #include string class Task { public: Task(const std::string who, int x, int y):_x(x), _y(y), _who(who) {} Task() {} void Execute() { _result _x _y; } std::string Result() { return std::to_string(_x) std::to_string(_y) std::to_string(_result); } ~Task() {} private: int _x; int _y; int _result; std::string _who; };testThread.cpp#include iostream #include cstdio #include string #include vector #include unistd.h #include pthread.h #include Task.hpp const int gsize 64; void *threadrun(void *args) { Task *t static_castTask *(args); t-Execute(); return t; } int main(int argc, char *argv[]) { if(argc ! 2) { std::cout argv[0] num std::endl; return 1; } int num std::stoi(argv[1]); std::vectorpthread_t tids; for(int i 0; i num; i) { sleep(1); pthread_t tid; char threadname[gsize]; snprintf(threadname, gsize, thread-%d, i1); Task *t new Task(threadname, 10i, 20*i); pthread_create(tid, nullptr, threadrun, threadname); tids.push_back(tid); std::cout create thread threadname done std::endl; } std::vectorTask* result_list; for(auto tid: tids) { Task *t; pthread_join(tid, (void **)t); result_list.push_back(t); std::cout join success: tid std::endl; } std::cout 处理结果清单: std::endl; for(auto res: result_list) { std::cout res-Result() std::endl; } return 0; }结果$ ./createThread 10 create threadthread-1 done create threadthread-2 done create threadthread-3 done create threadthread-4 done create threadthread-5 done create threadthread-6 done create threadthread-7 done create threadthread-8 done create threadthread-9 done create threadthread-10 done join success: 134049439938240 join success: 134049431545536 join success: 134049423152832 join success: 134049414760128 join success: 134049406367424 join success: 134049397974720 join success: 134049389582016 join success: 134049381189312 join success: 134049372796608 join success: 134049364403904 处理结果清单: 1701996660825058401-1767912235 1701996660825058401-1767912235 1701996660825058401-1767912235 1701996660825058401-1767912235 1701996660825058401-1767912235 1701996660825058401-1767912235 1701996660825058401-1767912235 1701996660825058401-1767912235 1701996660825058401-1767912235 1701996660825058401-17679122357. 分离线程默认情况下新创建的子线程是joinable可等待的。线程退出后必须对其进行pthread_join回收否则会导致系统资源泄漏。但如果我们完全不关心子线程的返回值阻塞等待反而会限制主线程的并发效率。这时我们可以利用线程分离告诉操作系统该线程退出时请自动释放其所有资源。7.1 函数原型int pthread_detach(pthread_t thread);分离可以是由线程组内其他线程对目标线程发起也可以是子线程自我分离pthread_detach(pthread_self());7.2 Joinable 与分离状态的冲突实证一个线程不能既是 joinable 又是分离的。让我们用代码实测强行join一个已分离的线程实测主线程分离子线程后强行 joinvoid *threadrun(void *args) { std::string name static_castconst char *(args); int cnt 3; while (cnt) { std::cout name is running std::endl; cnt--; sleep(1); } std::cout name is quit... std::endl; return nullptr; } int main() { pthread_t tid; pthread_create(tid, nullptr, threadrun, (void *)thread-1); pthread_detach(tid); sleep(1); int n pthread_join(tid, nullptr); std::cout main thread, n n std::endl; }结果$ ./createThread thread-1 is running main thread, n 22实测子线程自我分离后主线程强行 joinvoid *threadrun(void *args) { pthread_detach(pthread_self()); std::string name static_castconst char *(args); int cnt 3; while (cnt) { std::cout name is running std::endl; cnt--; sleep(1); } std::cout name is quit... std::endl; return nullptr; } int main() { pthread_t tid; pthread_create(tid, nullptr, threadrun, (void *)thread-1); sleep(1); int n pthread_join(tid, nullptr); std::cout main thread, n n std::endl; }以上两份测试代码的运行结果高度一致thread-1 is running main thread, join return n 22深层内核原理解释我们看到无论是谁发起的 detach当主线程强行等待一个已经被分离的子线程时pthread_join没有阻塞而是立刻返回并带回了错误码n 22。 我们在 Linux 系统底层的系统错误码文件/usr/include/asm-generic/errno-base.h中可以找到如下定义#define EINVAL 22 /* Invalid argument */这铁证如山地表明对于一个已经处于分离状态detached的线程试图通过pthread_join进行阻塞等待回收是一项非法参数操作EINVAL, Invalid argumentAPI 会立即抛出错误码返回。该子线程退出时其 TCB 结构和栈资源会自动由系统内核安全收回。本章完。