爱和冰阔落头像
关注
【Linux】pthread_t 到底是什么?从线程控制块、线程栈到 clone,把 NPTL 一次追到底封面图

【Linux】pthread_t 到底是什么?从线程控制块、线程栈到 clone,把 NPTL 一次追到底

封面

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

在这里插入图片描述


🏠博主简介
在这里插入图片描述


前言

前几篇把线程的概念、pthread 的接口都过了一遍,但一直有个疙瘩没解开:

pthread_create 返回的是 pthread_tps -aL 里看到的是 LWP;线程函数的返回值能被 pthread_join 拿到;pthread_detach 一设置,join 就注定失败——这些行为,接口层面只能"记住",解释不了。

这一篇不再停在 API 层,直接沿着 NPTL 往下追:struct pthreadALLOCATE_STACKmmapcreate_threaddo_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:

对比项LWPpthread_t
属于哪个层面内核调度层NPTL 线程库(用户层)
本质task_struct 里的编号一个虚拟地址
作用域系统级,全局唯一进程级,内核不认识
谁在使用它CPU 调度器、ps -aLpthread 系列接口
主线程特例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——相当于让多个人帮我带饭,小明带包子、小李带奶茶,而我在上层只维护一份结构数据即可。

三个核心问题(这一篇的总纲)

  1. 线程 ID 是什么? —— pthread_create 在库中创建的描述线程控制块的起始虚拟地址
  2. 线程传参和返回值? —— 线程执行完将退出结果写到控制块的 void *ret,通过 join 回收
  3. 线程分离? —— 控制块有 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] 换成局部数组,现象就坏了。机制拆开看:

  1. for 循环创建线程几乎瞬间完成,没有间隔
  2. 新线程创建后先 sleep(1) 才执行打印
  3. sleep 等待期间,for 循环已经开始创建线程 1、线程 2……每次 snprintf 都在覆盖同一个数组
  4. sleep 时间一到,所有新线程拿着同一个数组的地址去读,读到的全是最后一次写入的内容

这就是在进程间通信里见过的老问题换了个马甲:多个执行流共享公共资源且没有加保护——数据不一致问题。 只不过这次共享的"公共资源"是一个小小的栈上数组。

这里真正要保证的是:传给线程的数据,在线程真正读取之前必须仍然有效,同时避免多个线程无保护地修改同一份数据。做法上,让每个线程拿到独占的、生命周期足够长的内存(比如这里的堆空间),或者把数据拷贝进线程内部再释放,都行。

2.3 给线程起名字,让 ps 直接看到

批量创建时用 thread-0thread-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 指针作为第一个参数,实际签名变成了三参数,和接口要求对不上。

解决办法分两步:

  1. 加 static:去掉 this 指针,签名匹配了;但 static 函数访问不到成员属性
  2. 把 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_1ALLOCATE_STACKdo_clone 等)是那个时期的内部名字。不同 glibc 版本的内部函数名和调用层次会有变化(新版里 create_thread 直接走 __clone_internal),但 pthread_t、线程描述结构、线程栈、TLS 与内核执行流之间的整体关系没变。

pthread_t、线程描述结构与地址空间布局

pthread 库、线程描述结构与内核 LWP 的关系

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所属进程,所有线程一致
joinidjoin 等待的目标——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真正的"线程"标志:共享 tgidps -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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--