
🔥个人主页:爱和冰阔乐
📚专栏传送门:《数据结构与算法》 、C++、 《Linux操作系统》
🐶学习方向:C++方向学习爱好者
⭐人生格言:得知坦然 ,失之淡然

🏠博主简介

文章目录
前言
前几篇把线程的概念、pthread 的接口都过了一遍,但一直有个疙瘩没解开:
pthread_create 返回的是 pthread_t,ps -aL 里看到的是 LWP;线程函数的返回值能被 pthread_join 拿到;pthread_detach 一设置,join 就注定失败——这些行为,接口层面只能"记住",解释不了。
这一篇不再停在 API 层,直接沿着 NPTL 往下追:struct pthread、ALLOCATE_STACK、mmap、create_thread、do_clone,最后一直到 Linux 的 clone 系统调用。追完你会发现,join、detach、返回值、线程栈、TLS,全都在线程控制块里有落点。
一、pthread_t 到底是什么
1.1 先看现象:两个对不上的"线程 ID"
随手打印一下 pthread_create 出来的 tid,再开一个终端 ps -aL,两边看到的数字完全对不上:
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void *)"thread-1");
printf("tid: 0x%lx\n", tid); // 一串又长又大的十六进制数
ps -aL 里的 LWP 是 195829 这种小数字,而 tid 是 0x7f... 开头的大地址。两边对不上是正常的,它们本来就不是一套 ID:
| 对比项 | LWP | pthread_t |
|---|---|---|
| 属于哪个层面 | 内核调度层 | NPTL 线程库(用户层) |
| 本质 | task_struct 里的编号 | 一个虚拟地址 |
| 作用域 | 系统级,全局唯一 | 进程级,内核不认识 |
| 谁在使用它 | CPU 调度器、ps -aL | pthread 系列接口 |
| 主线程特例 | LWP == PID | 同样是库里的控制块地址 |
CPU 调度的时候看的是 LWP,因为 Linux 内核里只有轻量级进程。
pthread_t是库维持的进程内唯一标识,内核根本不认识它。
1.2 LWP 与 pthread_t 不是同一回事
- LWP 属于进程调度的范畴,是操作系统调度器的最小单位
- pthread_t 属于 NPTL 线程库的范畴,线程库的后续操作(join、detach、cancel)根据这个 ID 来操作线程
对于 Linux 目前的 NPTL 实现而言,pthread_t 类型的线程 ID,本质就是一个进程地址空间上的一个地址:

pthread_self 返回的是 pthread 库维持的进程内唯一标识。由于每个进程有自己独立的内存空间,此 ID 的作用域是进程级而非系统级。
1.3 动态库与线程控制块:库也要"先描述再组织"
那这个地址指向的是什么?要回答这个,得先看线程库本身在哪儿。
我们写的代码是 ELF 可执行程序,pthread 动态库也是 ELF 格式的库。可执行程序运行时需要将代码数据和动态库加载到内存中,并将动态库映射到当前进程的地址空间中——库在地址空间中有了起始虚拟地址,再根据偏移量访问库的内容:

进程自己的代码区可以访问到 pthread 库内部的函数或数据。 线程的概念在库里维护,库内部存在多个被创建好的线程,所以库也要管理线程——先描述再组织:
struct tcb
{
// 线程应有的属性
// 线程状态、线程 ID、线程独立的栈结构、线程大小...
};
注意:优先级、时间片、上下文并不存在 tcb 结构体里。这些与调度相关的属性写入到内核的 LWP→PCB 中。
线程的概念一部分在内核里实现,一部分在用户层实现。 这句话是这一篇的主线,后面所有内容都是在展开它。
在库中调 pthread_create 创建一个描述线程的结构,这就和调用 fopen 时 C 标准库在内部申请 FILE 对象、填充属性是一回事,库管理东西的套路都一样:先描述再组织。
1.4 库与 LWP 的联动:join、detach、返回值的落点
每次创建一个线程,通过 mmap 技术在库里申请空间,创建描述 TCB 的结构(包含 struct pthread、线程局部存储、线程栈),整个数据块通过数组管理起来。给用户的 pthread_create 返回值不是 LWP,而是线程在库中对应管理块的虚拟地址。
在 struct pthread(线程控制块)里有属性 void *ret。线程代码执行完 return 时,将返回值写到该线程控制块的 ret 中。线程运行结束后控制块并没有被释放,需要 join 来回收——join 传入 tid(控制块起始地址),从控制块里将退出结果带出来,然后释放控制块。
那内核里的 LWP 是谁创建的?pthread_create 除了在库里创建管理块,还要在内核中创建轻量级进程,即调用系统调用,这个系统调用就是 clone:

用户线程只需要在库里创建好描述线程相关的属性,给底层指明要执行什么方法,临时数据保存在线程栈里。用户空间里的属性无需修改,在用户视角是线程,真正执行的是内核的轻量级进程。唯一要做的就是线程执行完毕了,将结果写回到用户 ret 里面。
场景理解:中午我在宿舍打游戏,不想去买饭,让小明帮我买饭。我在大脑里记录了结构体(人物:小明,行为:买饭,类型:包子)。真正去买饭的是小明——库就是宿舍:在库中创建线程的描述结构体,然后躺平,底层自动执行,结果写到
ret里供上层拿。
Linux 用户级线程:内核 LWP = 1:1。 其他操作系统可能存在一个用户级线程对应多个内核级 LWP——相当于让多个人帮我带饭,小明带包子、小李带奶茶,而我在上层只维护一份结构数据即可。
三个核心问题(这一篇的总纲)
- 线程 ID 是什么? —— pthread_create 在库中创建的描述线程控制块的起始虚拟地址
- 线程传参和返回值? —— 线程执行完将退出结果写到控制块的
void *ret,通过 join 回收 - 线程分离? —— 控制块有
int joinable属性,默认为 1(必须 join),设为 0 即 detach(库自动释放)
这三条先记住,第五节追源码时会逐一在 struct pthread 里找到对应成员。
二、批量创建线程与传参的坑
2.1 批量创建多线程
#include <vector>
const int num = 10;
void *routine(void *args)
{
sleep(1);
std::string name = static_cast<const char *>(args);
delete (char *)args;
int cnt = 5;
while (cnt--)
{
std::cout << "new 线程名字: " << name << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
std::vector<pthread_t> tids;
for (int i = 0; i < num; i++)
{
pthread_t tid;
char *id = new char[64]; // 每个线程独立堆空间
snprintf(id, 64, "thread-%d", i);
if (pthread_create(&tid, nullptr, routine, id) == 0)
tids.push_back(tid);
}
for (int i = 0; i < num; i++)
pthread_join(tids[i], nullptr);
return 0;
}

这里给每个线程 new 了一段堆空间。虽然堆也是所有线程共享的,但只有拿到起始地址的那个线程知道去哪里读,所以各自打印互不干扰。
2.2 踩坑点:为什么不能用局部数组?
char id[64]; // ❌ 局部临时数组
snprintf(id, 64, "thread-%d", i);
pthread_create(&tid, nullptr, routine, id); // 传的是数组起始地址
把 new char[64] 换成局部数组,现象就坏了。机制拆开看:
- for 循环创建线程几乎瞬间完成,没有间隔
- 新线程创建后先
sleep(1)才执行打印 - sleep 等待期间,for 循环已经开始创建线程 1、线程 2……每次 snprintf 都在覆盖同一个数组
- sleep 时间一到,所有新线程拿着同一个数组的地址去读,读到的全是最后一次写入的内容
这就是在进程间通信里见过的老问题换了个马甲:多个执行流共享公共资源且没有加保护——数据不一致问题。 只不过这次共享的"公共资源"是一个小小的栈上数组。
这里真正要保证的是:传给线程的数据,在线程真正读取之前必须仍然有效,同时避免多个线程无保护地修改同一份数据。做法上,让每个线程拿到独占的、生命周期足够长的内存(比如这里的堆空间),或者把数据拷贝进线程内部再释放,都行。
2.3 给线程起名字,让 ps 直接看到
批量创建时用 thread-0、thread-1 这种字符串传参,线程自己知道名字,但系统工具看不见。想让 ps -aL 直接显示线程名,用 pthread_setname_np:
void *hello(void *args)
{
char buffer[64];
pthread_setname_np(pthread_self(), "Thread-0"); // 给自己起名
pthread_getname_np(pthread_self(), buffer, sizeof(buffer) - 1);
while (true)
{
std::cout << "hello world, " << buffer << std::endl;
sleep(1);
}
}
运行程序,另开终端:
ps -aL | grep a.out
能看到这样的输出,线程名直接出现在系统命令里:
PID LWP TTY TIME COMMAND
195828 195828 pts/1 00:00:00 main
195828 195829 pts/1 00:00:00 Thread-0
195828 195830 pts/1 00:00:00 Thread-1
用的时候注意几点。_np 是 non-portable 的意思,这套接口 Linux 和部分类 Unix 系统才有;名字最大 16 个字符(包括结尾的 \0),超过长度时函数返回 ERANGE 报错,不是悄悄截断;第一个参数是 pthread_t,所以理论上也能给别的线程改名——man 手册里的示例就是主线程给新建的线程起名——不过实际调试里配 pthread_self() 自己给自己起名用得最多。
调试多线程程序时,日志里配合线程名打印,比一堆裸 LWP 号好认得多。
三、线程封装(Thread.hpp)
3.1 为什么要封装
原生 pthread 接口是 C 风格的:入口函数签名写死成 void*(*)(void*),线程状态散落在调用方代码里。把线程操作封装成类,状态(是否运行、是否分离)收进成员变量,用起来就是 C++ 的味道了:
#ifndef _THREAD_H_
#define _THREAD_H_
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <functional>
namespace ThreadModlue
{
static uint32_t number = 1; // 计数器,用于生成线程名
class Thread
{
using func_t = std::function<void()>;
private:
void EnableDetach() { _isdetach = true; }
void EnableRunning() { _isrunning = true; }
static void *Routine(void *args) // static 去掉 this 指针
{
Thread *self = static_cast<Thread *>(args);
self->EnableRunning();
if (self->_isdetach)
self->Detach();
self->_func(); // 回调处理
return nullptr;
}
public:
Thread(func_t func)
: _tid(0), _isdetach(false), _isrunning(false),
res(nullptr), _func(func)
{
_name = "thread-" + std::to_string(number++);
}
void Detach()
{
if (_isdetach) return;
if (_isrunning) pthread_detach(_tid);
EnableDetach();
}
bool Start()
{
if (_isrunning) return false;
int n = pthread_create(&_tid, nullptr, Routine, this);
if (n != 0)
{
std::cerr << "create thread error:" << strerror(n) << std::endl;
return false;
}
std::cout << _name << " create success" << std::endl;
return true;
}
bool Stop()
{
if (_isrunning)
{
int n = pthread_cancel(_tid);
if (n != 0) return false;
_isrunning = false;
std::cout << _name << " stop" << std::endl;
return true;
}
return false;
}
void Join()
{
if (_isdetach)
{
std::cout << "线程已分离,不能 join" << std::endl;
return;
}
int n = pthread_join(_tid, &res);
if (n != 0)
std::cerr << "Join error: " << strerror(n) << std::endl;
else
std::cout << "join success" << std::endl;
}
~Thread() {}
private:
pthread_t _tid;
std::string _name;
bool _isdetach;
bool _isrunning;
void *res;
func_t _func;
};
}
#endif
3.2 关键设计:为什么 Routine 必须是 static
整个封装里最容易出问题的是这一行:
static void *Routine(void *args)
pthread_create 要求入口函数的签名是 void *(*)(void *)。而类的非静态成员函数,编译器会默认塞一个 this 指针作为第一个参数,实际签名变成了三参数,和接口要求对不上。
解决办法分两步:
- 加 static:去掉 this 指针,签名匹配了;但 static 函数访问不到成员属性
- 把 this 当参数传进去:
pthread_create(..., Routine, this),线程内部static_cast<Thread *>(args)还原,成员访问全通过self->完成
这个套路在所有"给 C 回调接口包 C++ 类"的场景都会遇到,记住它:static 函数 + this 转参。
还有一个细节:Routine 里判断了 _isdetach——如果 Detach 发生在 Start 之前,pthread_detach 还没有 tid 可用,就先只置标志位,等线程真正跑起来后在 Routine 内部补一次 self->Detach()。分离时机早于启动时机这个坑,就这样填掉了。
3.3 使用
#include "Thread.hpp"
using namespace ThreadModlue;
int main()
{
Thread t([]() {
while (true)
{
std::cout << "我是一个新线程" << std::endl;
sleep(1);
}
});
t.Detach();
t.Start();
sleep(5);
t.Stop();
sleep(5);
t.Join(); // 已分离,join 失败并给出提示
}
四、线程局部存储(TLS)
4.1 用 __thread 让每个线程各有一份全局变量
控制块里的三样东西——struct pthread、线程栈、线程局部存储——前两个都见过了,TLS 是最后一块。用法很简单,在全局变量前加 __thread:
__thread int count = 1; // 线程局部存储
std::string Addr(int &c)
{
char addr[64];
snprintf(addr, sizeof(addr), "%p", &c);
return addr;
}
void *routine1(void *args)
{
while (true)
{
std::cout << "thread-1, count=" << count
<< ", &count: " << Addr(count) << std::endl; // 修改方
count++;
sleep(1);
}
}
void *routine2(void *args)
{
while (true)
{
std::cout << "thread-2, count=" << count
<< ", &count: " << Addr(count) << std::endl; // 只读方
sleep(1);
}
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, routine1, nullptr);
pthread_create(&tid2, nullptr, routine2, nullptr);
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}

现象:线程 1 一路把 count 加上去,线程 2 的 count 永远是 1;打印出来的地址也不一样。
原理:加了 __thread,会把原本定义在已初始化数据段的全局变量,挪到每个线程自己的局部存储中开辟。多个线程各自有独立的副本——名字一样,底层虚拟地址不同,每个线程都拥有这个变量自己的实例。
4.2 意义与限制
线程局部存储的意义:希望有全局变量(获取线程 ID、计数器、缓存等),但不想被其他线程看到。类型上
__thread并不限定基本类型,GCC 文档里就有extern __thread struct state s;的用法;真正要注意的是 C++ 里的限制——比如初始化器得是编译期常量、带非平凡构造/析构的对象用起来要小心。
pthread_setname_np / pthread_getname_np 这对接口可以理解为:把设定的字符串放到线程私有的存储里,只有本线程相关的执行流会去读写。线程私有数据只有本线程能访问,不存在并发问题,这也是它能安全工作的原因。
主线程在 Linux 里跑起来,本质上也是把 main 的地址和栈交给执行流去调度。新线程用的则是库 mmap 出来的线程栈——每个线程都有自己的独立栈结构,主线程是地址空间里的原栈,新线程是管理块里的栈。这个区分在第七节追源码时会再次出现。
五、顺着 pthread_create 往下追:pthread_t 从哪来
接口都会用了,现在进 glibc 的 NPTL 实现里追一层,pthread_t、线程描述结构和 clone 就能接在一起。先用两张图把关系摆出来:第一张看 pthread_t、线程描述结构和进程地址空间的位置关系;第二张看 pthread 库与内核 LWP 是怎样接上的。
说明:下面源码以较早版本 glibc 的 NPTL 实现为参照,函数名(
__pthread_create_2_1、ALLOCATE_STACK、do_clone等)是那个时期的内部名字。不同 glibc 版本的内部函数名和调用层次会有变化(新版里create_thread直接走__clone_internal),但pthread_t、线程描述结构、线程栈、TLS 与内核执行流之间的整体关系没变。


5.1 pthread_create 的实现骨架
int __pthread_create_2_1(pthread_t *newthread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg)
{
const struct pthread_attr *iattr = (struct pthread_attr *)attr;
if (iattr == NULL)
iattr = &default_attr;
struct pthread *pd = NULL;
// 先准备线程描述结构和线程栈
int err = ALLOCATE_STACK(iattr, &pd);
if (err != 0)
return err;
// 保存线程将要执行的函数和参数
pd->start_routine = start_routine;
pd->arg = arg;
// pthread_t 与线程描述结构建立联系
*newthread = (pthread_t)pd;
// 再向下创建真正的线程执行流
err = create_thread(pd, iattr, STACK_VARIABLES_ARGS);
return err;
}
看这段代码时盯住几处:线程库先准备 struct pthread 和它的栈;用户传进来的线程入口函数和参数,被保存到线程描述结构中;返回给用户的 pthread_t,就是这个描述结构的地址。第一节的所有疑问,到这里在源码里找到了答案。
5.2 struct pthread:把接口行为逐个对上
pthread_attr 保存的是用户可设置的属性:调度参数、分离状态、保护区大小、栈地址和栈大小等:
struct pthread_attr
{
struct sched_param schedparam;
int schedpolicy;
int flags;
size_t guardsize;
void *stackaddr;
size_t stacksize;
cpu_set_t *cpuset;
size_t cpusetsize;
};
线程描述结构本身信息更多,抓和前面内容直接相关的成员:
struct pthread
{
pid_t tid;
pid_t pid;
struct pthread *joinid;
void *result;
void *(*start_routine)(void *);
void *arg;
void *stackblock;
size_t stackblock_size;
size_t guardsize;
// 其余成员省略
};
逐个和第一节的"三个核心问题"对上:
| 成员 | 对应的接口行为 |
|---|---|
tid | 内核里轻量级进程的编号,pthread_cancel 靠它找到目标 |
pid | 所属进程,所有线程一致 |
joinid | join 等待的目标——join 谁等谁,就存在这里 |
result | 线程 return / pthread_exit 的退出结果写到这里,join 取走的就是它 |
start_routine / arg | 入口函数与传参,create 时填入 |
stackblock / stackblock_size | 线程栈的位置和大小(第七节展开) |
guardsize | 栈尾保护区大小,防止栈溢出踩到别的内存 |
joinid 和 result 这两个字段,就是
pthread_join能拿到退出结果、join 必须等人的全部底气;detach 改的 joinable 语义也落在这组字段上。
六、create_thread 最后还是要走 clone
线程库把用户层的描述信息准备好后,进入 create_thread。这里会组合一组 clone 标志:
int clone_flags =
CLONE_VM
| CLONE_FS
| CLONE_FILES
| CLONE_SYSVSEM
| CLONE_SIGHAND
| CLONE_THREAD
| CLONE_SETTLS
| CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID;
这组标志和"线程共享什么资源"逐条对上(以当前 glibc NPTL 的 pthread_create 为例):
| 标志 | 含义 | 对应的线程语义 |
|---|---|---|
CLONE_VM | 共享地址空间 | 看到同一个页表、同一份代码和全局数据 |
CLONE_FS | 共享根目录、当前工作目录 | chdir 全线程可见 |
CLONE_FILES | 共享文件描述符表 | 一个线程 open,其他线程能用 |
CLONE_SIGHAND | 共享信号处理函数表 | 信号处理函数全线程一致 |
CLONE_THREAD | 真正的"线程"标志:共享 tgid | ps -L 里同 PID 下出现多个 LWP,也不产生独立的 wait 语义 |
CLONE_SETTLS | 为新线程设置 TLS | 第四节的 __thread 副本就靠它 |
CLONE_PARENT_SETTID | 把新执行流的号写回给父 | 库拿到内核侧的编号 |
CLONE_CHILD_CLEARTID | 线程退出时把 tid 内存清零并唤醒等待者 | join 能感知退出的底层机制 |
CLONE_SYSVSEM | 共享 System V 信号量 | 保留 IPC 语义 |
单独说一句 CLONE_CHILD_CLEARTID:线程退出时内核把用户态 tid 指向的内存清零,并唤醒在这个地址上等待的线程——pthread_join 的"感知线程结束"就是等在这里。去掉全套线程语义、只留这个标志,join 就退化成了内核可观测的事件。把它记下来,后面再看 join 的实现就不会觉得突兀。
调用链继续往下:
pthread_create
↓
__pthread_create_2_1
↓
ALLOCATE_STACK
↓
create_thread
↓
do_clone
↓
ARCH_CLONE
↓
__clone
↓
clone 系统调用
↓
内核创建轻量级进程
在 x86_64 的封装里,ARCH_CLONE 对应 __clone。汇编封装整理参数和新线程栈,取得 clone 系统调用号,最终通过 syscall 陷入内核。(上面的 do_clone → ARCH_CLONE → __clone 是较早版本 glibc 的路径;新版 glibc 里 create_thread 直接调 __clone_internal,中间少几层包装,最终进的是同一个 clone/clone3 系统调用。)
pthread 库负责把线程"包装成好用的接口",真正的执行流创建和调度仍然落到 Linux 内核。
七、线程栈是怎样通过 mmap 申请出来的
ALLOCATE_STACK 最终走到 allocate_stack。用户没有自己指定栈时,就使用默认大小,在进程地址空间内部申请。关键一步是 mmap:
mem = mmap(NULL, size, prot,
MAP_PRIVATE | MAP_ANONYMOUS | ARCH_MAP_FLAGS,
-1, 0);
申请到空间后,线程描述结构放在这块空间的对应位置,同时记录:
pd->stackblock = mem;
pd->stackblock_size = size;
也就是说,新线程的栈不是重新创建一套地址空间,而是在当前进程地址空间中为新的执行流准备一块栈空间。主线程栈和新线程栈的区别用一张表收束:
| 对比项 | 主线程栈 | 新线程栈 |
|---|---|---|
| 来源 | 进程地址空间原有的栈区 | pthread 库 mmap 申请(共享区内) |
| 大小 | 可动态扩容,超上限才段错误 | 创建时固定(默认 8M),用尽即止 |
| 归属 | fork 时复制而来,COW | 控制块管理,stackblock 记录位置 |
主线程
└─ 使用进程原有的主栈
新线程
└─ pthread 库在地址空间中申请自己的栈
↓
准备 struct pthread / TLS 等线程信息
↓
clone 创建内核执行流
顺带一提:
vm_area_struct会为这块新区域登记。从内核视角看,它就是地址空间里多出来的一个普通 VMA,和别的映射区没有任何特殊待遇。
八、最后自己调用一次 clone
把"pthread 底层最终使用 clone"这件事单独看一眼:自己准备一块栈,直接让 clone 执行指定函数:
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024) // 1MB 的栈空间
static int child_func(void *arg)
{
printf("Child process: PID = %d\n", getpid());
return 0;
}
int main()
{
char *stack = (char *)malloc(STACK_SIZE);
if (stack == NULL)
{
perror("malloc");
exit(EXIT_FAILURE);
}
pid_t pid = clone(child_func,
stack + STACK_SIZE, // 注意:传高地址
CLONE_VM | SIGCHLD,
NULL);
if (pid == -1)
{
perror("clone");
free(stack);
exit(EXIT_FAILURE);
}
printf("Parent process: PID = %d, Child PID = %d\n",
getpid(), pid);
waitpid(pid, NULL, 0);
free(stack);
return 0;
}
预期输出:
Parent process: PID = 31234, Child PID = 31235
Child process: PID = 31235
有几处容易想岔。传 stack + STACK_SIZE 而不是 stack,是因为栈向下生长,起始地址要用这块内存的高端,传 stack 本身的话第一次压栈就写越界了。waitpid 能用,是因为 clone 的返回值就是新执行流的 PID;这里没带 CLONE_THREAD,内核眼里它还是一个"子进程",只是和父进程共享地址空间,所以等 SIGCHLD 的老流程照常走。最后可以自己改着玩:把 CLONE_VM 去掉再跑一次,新执行流不共享地址空间,行为立刻就贴近 fork 出来的子进程了。
这个实验不承担完整线程语义(没有 TLS、没有控制块、没有 joinable),它的价值就是把最后一层关系看清:
用户层线程接口 pthread_create
↓
pthread 库
↓
clone
↓
Linux 内核中的轻量级执行流
pthread 库做封装、补语义,内核负责创建和调度执行流。
总结
这一组线程文章最后收成这条调用链:
用户代码
↓
pthread_create
↓
NPTL / struct pthread
↓
准备线程栈、TLS、属性
↓
create_thread / do_clone
↓
clone
↓
Linux 内核创建轻量级执行流
回到开头那三个问题:
- pthread_t:在 Linux glibc/NPTL 当前实现中,本质对应线程描述结构
struct pthread的地址;进程级标识,不是内核的 LWP ID - 返回值:写进控制块的
result,join 从这里取 - 分离:控制块的 joinable 语义,库负责识别退出并自动释放
上层看到的是线程接口,线程库负责组织和封装,内核真正负责创建与调度 LWP。前面关于地址空间、页表、线程栈和 pthread_t 的内容,到这里就全部接在了一起。
资源分享:
【Linux】线程到底是什么?从轻量级进程、虚拟地址到页表与 MMU,一次理清线程底层模型
【Linux】malloc 1GB 内存为什么没立刻占满?从缺页、COW 到 Cache/TLB,看懂线程为什么更轻
【Linux】多线程打印为什么会乱?pthread 创建、等待、退出、取消与分离全实战
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2402_87731470/article/details/164205396




