Linux NPTL线程:创建/终止/等待/分离/栈布局内核原理|线程下篇

Linux NPTL线程:创建/终止/等待/分离/栈布局内核原理|线程下篇

前言

在上篇中,我们已经建立了Linux线程的底层心智模型:线程在内核视角本质上是轻量级进程LWP,多个线程通过共享同一个虚拟地址空间形成执行流;而虚拟地址空间、页表、TLB、缺页异常,则是理解线程资源共享与私有边界的基础。

但只懂内核原理还不够。真正写Linux多线程程序时,我们面对的不是task_struct,而是POSIX pthread用户态库。为什么编译时要加-lpthread?为什么pthread_t不是内核LWP?为什么子线程栈不能动态增长?为什么一个线程调用exit()会杀死整个进程?

本文作为【Linux线程专题下篇】,聚焦用户态POSIX pthread线程库,从API实操、线程ID、虚拟地址栈布局、C++面向对象封装,一路向下拆解到glibc NPTL底层与clone系统调用。目标不是背API,而是建立"用户态调用 → 库内部处理 → 内核创建LWP"的完整链路。

👉 点击阅读上篇:Linux线程专题(上篇):Linux线程深度剖析:轻量级进程、虚拟内存分页、进程线程资源对比


三、Linux线程控制 --- POSIX pthread库实操

3.1 pthread库基础与错误处理

Linux内核并没有直接提供"线程系统调用",而是提供更通用的clone系统调用。用户态线程能力由POSIX标准线程库libpthread.so提供,Linux主流实现是NPTL(Native POSIX Thread Library)。

使用pthread的基础要求:

  • 头文件:#include <pthread.h>
  • 编译链接必须加:-lpthread

⚠️【易错点】很多初学者第一次写多线程代码,逻辑没有问题,却报undefined reference to pthread_create。这不是代码错,而是没有链接pthread动态库。

bash 复制代码
gcc demo.c -o demo -lpthread

pthread函数的错误处理规则非常重要:

  • 成功时返回 0
  • 失败时直接返回正数错误码
  • 不会修改全局errno
  • 打印错误信息应使用 strerror(返回值),而不是perror()
c 复制代码
int ret = pthread_create(&tid, nullptr, func, nullptr);
if (ret != 0)
{
    fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
}

【通俗理解】为什么pthread不修改全局errno?

因为全局errno本质上是进程级全局变量。如果多个线程同时出错,都会去改写同一个errno,结果就是错误信息互相覆盖。pthread把错误码作为返回值直接返回,更适合多线程环境;同时库内部仍会维护线程私有errno,以兼容部分旧代码。但业务上最规范的方式,仍然是直接判断函数返回值。


3.2 线程创建 pthread_create

函数原型
c 复制代码
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void*), void *arg);

四个参数逐一看:

  1. thread:输出参数,用于保存新创建线程的用户态ID pthread_t
  2. attr:线程属性结构体,传NULL表示使用默认属性
  3. start_routine:线程入口函数,签名必须是 void* (*)(void*)
  4. arg:传递给入口函数的参数,只能传一个void*

如果需要给线程传多个参数,不能直接传多个指针,而应该把参数封装成一个结构体,再把结构体指针作为arg传入。

入口函数的约束

线程入口函数有严格签名要求:

c 复制代码
void* thread_routine(void* arg);

这意味着:

  • 返回值必须是 void*
  • 参数必须是 void*
  • 线程拿到arg后,再根据业务需要强转成具体类型

⚠️【易错点】不要把普通成员函数、类成员函数直接塞给pthread_create。C++普通成员函数会隐式携带this指针,函数签名不匹配。

获取当前线程ID pthread_self
c 复制代码
pthread_t pthread_self(void);

这个函数返回的是当前线程的用户态库ID,不是内核LWP。

⚠️【易错点】pthread_t 只在当前进程内部有意义,内核并不识别它。它更像pthread库内部用来索引线程对象的"句柄",而不是系统全局ID。

【通俗理解】pthread_t 就像公司内部的员工工号,只在公司内部有效;而LWP更像操作系统给每个轻量级进程发的全局身份证号。

查看内核LWP:ps -aL

线程创建后,可以通过:

bash 复制代码
ps -aL

查看进程内所有轻量级进程。

关键字段:

  • PID:进程ID
  • LWP:轻量级进程ID

主线程有一个重要特征:主线程的LWP等于PID。因为主线程本来就是进程最初的执行流,内核调度ID和进程ID相同。

完整C语言示例
c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

void* thread_entry(void* arg)
{
    const char* msg = (const char*)arg;
    printf("子线程运行,pthread_t: 0x%lx, 消息: %s\n", pthread_self(), msg);
    sleep(2);
    return nullptr;
}

int main(void)
{
    pthread_t tid;
    int ret = pthread_create(&tid, nullptr, thread_entry, (void*)"hello pthread");
    if (ret != 0)
    {
        fprintf(stderr, "create thread failed: %s\n", strerror(ret));
        return 1;
    }

    printf("主线程等待子线程...\n");
    pthread_join(tid, nullptr);
    printf("主线程:子线程执行完毕\n");
    return 0;
}

编译命令:

bash 复制代码
gcc demo.c -o create_demo -lpthread

3.3 线程的三种终止方式

线程终止有三种常见方式,但它们的语义并不相同。

⚠️【易错点】子线程绝对不要调用exit() 。exit()是进程级退出函数,一旦调用,整个进程所有线程都会被销毁。

方式一:线程入口函数 return

线程执行流执行到return时,当前线程终止。

c 复制代码
void* func_return(void* arg)
{
    int* p = (int*)malloc(sizeof(int));
    *p = 100;
    printf("[return] 子线程执行完毕\n");
    return p;
}

【通俗理解】普通子线程return,只是当前执行流结束;但main函数return不一样,它等价于exit(),会直接终止整个进程。

方式二:pthread_exit
c 复制代码
void pthread_exit(void *value_ptr);

线程主动调用该函数,只终止当前线程,不影响进程内其他线程。

c 复制代码
void* func_exit(void* arg)
{
    int* p = (int*)malloc(sizeof(int));
    *p = 200;
    printf("[pthread_exit] 子线程执行完毕\n");
    pthread_exit(p);
}

⚠️【易错点】value_ptr不能指向当前线程栈局部变量。线程退出后,线程栈可能被回收,栈变量地址会变成野指针。

正确做法:

  • 使用全局变量
  • 使用malloc/new分配堆内存
方式三:pthread_cancel
c 复制代码
int pthread_cancel(pthread_t thread);

一个线程可以调用pthread_cancel请求终止另一个线程。

但它不是"立刻杀死",而是向目标线程发送取消请求。目标线程通常要运行到某个取消点(如sleep、read、write等),才会真正退出。

c 复制代码
void* func_cancel(void* arg)
{
    while (1)
    {
        sleep(1);
        printf("[cancel-test] 子线程运行中...\n");
    }
    return nullptr;
}

线程被取消后,pthread_join拿到的返回值是宏常量 PTHREAD_CANCELED。

三种终止方式完整示例
c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

void* func_return(void* arg)
{
    int* p = (int*)malloc(sizeof(int));
    *p = 100;
    printf("[return] 子线程执行完毕\n");
    return p;
}

void* func_exit(void* arg)
{
    int* p = (int*)malloc(sizeof(int));
    *p = 200;
    printf("[pthread_exit] 子线程执行完毕\n");
    pthread_exit(p);
}

void* func_cancel(void* arg)
{
    while (1)
    {
        sleep(1);
        printf("[cancel-test] 子线程运行中...\n");
    }
    return nullptr;
}

int main(void)
{
    pthread_t tid;
    void* res;

    pthread_create(&tid, nullptr, func_return, nullptr);
    pthread_join(tid, &res);
    printf("return 退出,返回值: %d\n", *(int*)res);
    free(res);

    pthread_create(&tid, nullptr, func_exit, nullptr);
    pthread_join(tid, &res);
    printf("pthread_exit 退出,返回值: %d\n", *(int*)res);
    free(res);

    pthread_create(&tid, nullptr, func_cancel, nullptr);
    sleep(2);
    pthread_cancel(tid);
    pthread_join(tid, &res);
    if (res == PTHREAD_CANCELED)
    {
        printf("线程被 pthread_cancel 取消\n");
    }

    return 0;
}

3.4 线程等待 pthread_join

为什么需要线程等待?

线程退出后,如果没有被等待,它的TCB(线程控制块)和线程栈资源通常不会立即归还系统,而是变成类似"僵尸"的状态。

【通俗理解】进程中子进程退出后,父进程不wait会产生僵尸进程;线程中,子线程退出后,主线程不join,也会产生僵尸线程。

这些资源如果长期不回收,会造成内存泄漏。因此,对于需要知道结果、或者需要保证执行顺序的线程,应该使用pthread_join等待。

函数原型
c 复制代码
int pthread_join(pthread_t thread, void **value_ptr);

参数说明:

  • thread:目标线程ID
  • value_ptr:输出参数,用于接收线程返回值
  • 如果不关心返回值,传NULL即可
不同终止方式下的返回值
线程终止方式 pthread_join 得到的 value_ptr
线程函数 return return 返回的指针
pthread_exit 退出 传给 pthread_exit 的参数
pthread_cancel 取消 PTHREAD_CANCELED

⚠️【易错点】返回值指针不能指向被等待线程的栈局部变量。线程退出后,栈空间可能失效,访问这类地址是典型野指针问题。

获取线程返回值示例
c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

typedef struct
{
    int a;
    int b;
} TaskArg;

typedef struct
{
    int sum;
    int product;
} TaskResult;

void* task_run(void* arg)
{
    TaskArg* p = (TaskArg*)arg;
    TaskResult* res = (TaskResult*)malloc(sizeof(TaskResult));
    res->sum = p->a + p->b;
    res->product = p->a * p->b;
    return res;
}

int main(void)
{
    pthread_t tid;
    TaskArg arg = {3, 5};
    void* ret;

    pthread_create(&tid, nullptr, task_run, &arg);
    pthread_join(tid, &ret);

    TaskResult* res = (TaskResult*)ret;
    printf("sum = %d, product = %d\n", res->sum, res->product);
    free(res);

    return 0;
}

编译命令:

bash 复制代码
gcc demo.c -o join_demo -lpthread

3.5 线程分离 pthread_detach

线程有两种基本状态:

  • joinable:可连接,退出后需要被pthread_join回收
  • detached:已分离,退出后资源自动回收

这两种状态是互斥的。一个线程不能同时既是joinable又是detached。

函数原型
c 复制代码
int pthread_detach(pthread_t thread);
两种分离方式
  1. 其他线程分离目标线程:
c 复制代码
pthread_detach(tid);
  1. 线程自己分离自己:
c 复制代码
pthread_detach(pthread_self());

【通俗理解】joinable就像死后需要别人来收尸;detached就像死后自动火化,不需要别人专门来收。

分离线程的行为
  • 线程终止后,TCB和线程栈由系统自动回收
  • 不需要也不应该再调用pthread_join
  • 对已经detach的线程调用pthread_join,会返回错误

⚠️【易错点】很多人以为"分离就是线程立即脱离调度",这是错的。detach改变的是线程退出后的资源回收方式,不是立刻把线程从CPU上撤下来。

使用场景
  • 用pthread_join:主线程需要等待子线程完成,或需要获取返回值
  • 用pthread_detach:后台工作线程,不需要等待结果,也不关心退出状态

四、线程ID及进程虚拟地址栈布局

4.1 pthread_t 与内核LWP的区别

这是Linux线程中最容易混淆的一组概念。

pthread_t 是什么?

在NPTL实现中,pthread_t本质上是一个用户态虚拟地址 ,指向库内部维护的线程控制块struct pthread。

它有几个关键特征:

  • 只在当前进程内有效
  • 内核不识别
  • 主要供pthread库内部管理线程使用

【通俗理解】pthread_t就像图书馆里的书籍借阅号,图书馆系统自己用它找书;操作系统不是图书馆系统,所以不认识这个号。

LWP是什么?

LWP(Light Weight Process)是内核视角的轻量级进程ID,对应task_struct中的tpid。

它的特征是:

  • 由clone系统调用创建
  • 是内核调度实体
  • 系统全局唯一
  • 可以通过ps -aL查看

Linux NPTL采用1:1线程模型:一个用户态pthread线程,对应一个内核LWP。

pthread_self() 和 ps -aL 分别拿到什么?
  • pthread_self():拿到用户态pthread_t
  • ps -aL:看到内核LWP ID

二者不是一回事。


4.2 主线程栈、子线程栈与Guard警戒保护页

主线程栈

主线程栈是进程原生栈,来自进程启动时的地址空间布局。

特点:

  • 位于进程虚拟地址空间的栈区
  • 支持动态向下增长
  • 栈空间不足时,内核可以通过缺页异常扩展栈区,直到达到栈上限

所以,主线程的局部变量递归较深时,不一定立刻崩溃,内核可能会帮它扩展栈。

子线程栈

子线程栈不是由内核自动扩展的,而是由pthread库主动创建的。

库内部会调用类似:

c 复制代码
mmap(NULL, stack_size,
     PROT_READ | PROT_WRITE,
     MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK,
     -1, 0);

在mmap动态映射区分配一块匿名内存,作为子线程栈。

特点:

  • 默认大小通常是8MB
  • 位于进程的mmap共享映射区
  • 大小固定,不能动态增长
  • 栈溢出会直接触发段错误

⚠️【易错点】这是面试高频坑。主线程栈可以动态增长,子线程栈不能。所以递归深度过大、局部数组过大,更容易把子线程栈撑爆。

Guard警戒保护页

分配子线程栈时,库通常还会在栈边界设置一块"保护页"。

保护页使用:

c 复制代码
mprotect(guard_page_addr, pagesize, PROT_NONE);

也就是这块页面不可读、不可写、不可执行。

【通俗理解】保护页就像栈旁边的"警戒线"。程序正常访问栈内页面没问题;一旦栈溢出踩进保护页,就会立刻触发段错误,帮助尽早暴露问题。

保护页大小可以通过线程属性调整,但默认通常是一个页面。

TCB的位置

struct pthread线程控制块并不独立放在别处,而是直接存放在子线程mmap分配出来的栈内存块内部。

也就是说,这块内存既放TCB,也放线程栈,还可能包含TLS线程局部存储。


五、C++面向对象封装线程

5.1 为什么普通成员函数不能直接作为线程入口?

这是C++封装线程时最核心的坑。

C++普通成员函数有一个隐式参数:this指针。所以它的真实签名并不是:

c 复制代码
void* (*)(void*)

而是类似:

c 复制代码
void* (Class::*)(void*)

这和pthread要求的函数指针类型不匹配。

【通俗理解】普通成员函数必须"绑在某个对象上"才能调用;pthread入口函数更像一个独立的普通函数,不要求对象身份。

✅ 正确做法:

  • 使用static静态成员函数作为线程入口
  • static函数没有隐式this
  • 通过arg参数把对象this指针传进去
  • 在static函数内部强转this,再调用普通成员函数

5.2 线程命名接口

c 复制代码
int pthread_setname_np(pthread_t thread, const char *name);
int pthread_getname_np(pthread_t thread, char *buf, size_t size);

注意点:

  • _np表示non-portable,是Linux非标准扩展
  • 线程名最大16字节,包含\0
  • 超长会被截断
  • 只用于调试,不能作为线程ID使用

5.3 Thread类封装实现

下面是一个更规范、可编译的Thread封装版本。

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <cstring>

namespace ThreadModule
{
    enum class TSTATUS
    {
        THREAD_NEW,
        THREAD_RUNNING,
        THREAD_STOP
    };

    using threadfunc_t = std::function<void()>;

    class Thread
    {
    private:
        static void* Routine(void* args)
        {
            Thread* self = static_cast<Thread*>(args);
            pthread_setname_np(pthread_self(), self->_name.c_str());
            self->_status = TSTATUS::THREAD_RUNNING;

            if (!self->_joinable)
            {
                pthread_detach(pthread_self());
            }

            if (self->_func)
            {
                self->_func();
            }

            return nullptr;
        }

    public:
        explicit Thread(threadfunc_t func)
            : _func(std::move(func)),
              _status(TSTATUS::THREAD_NEW),
              _joinable(true)
        {
            static uint32_t cnt = 0;
            _name = "Thread-" + std::to_string(cnt++);
        }

        ~Thread() = default;

        void EnableDetach()
        {
            if (_status == TSTATUS::THREAD_NEW)
            {
                _joinable = false;
            }
        }

        bool Start()
        {
            if (_status == TSTATUS::THREAD_RUNNING)
            {
                return true;
            }

            int ret = pthread_create(&_id, nullptr, Routine, this);
            if (ret != 0)
            {
                std::cerr << "pthread_create failed: " << strerror(ret) << std::endl;
                return false;
            }

            return true;
        }

        bool Join()
        {
            if (!_joinable)
            {
                std::cerr << "thread is detached, cannot join" << std::endl;
                return false;
            }

            int ret = pthread_join(_id, nullptr);
            if (ret != 0)
            {
                std::cerr << "pthread_join failed: " << strerror(ret) << std::endl;
                return false;
            }

            _status = TSTATUS::THREAD_STOP;
            return true;
        }

    private:
        pthread_t _id;
        std::string _name;
        TSTATUS _status;
        bool _joinable;
        threadfunc_t _func;
    };
}
测试代码
cpp 复制代码
#include "Thread.hpp"
#include <unistd.h>

void task_work()
{
    char buf[64] = {0};
    pthread_getname_np(pthread_self(), buf, sizeof(buf) - 1);
    while (true)
    {
        std::cout << "worker thread running: " << buf << std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_setname_np(pthread_self(), "main-thread");

    ThreadModule::Thread t1(task_work);
    // t1.EnableDetach();
    t1.Start();
    t1.Join();

    return 0;
}
Makefile
makefile 复制代码
all:main
main:main.cc
	g++ -o $@ $^ -std=c++11 -lpthread
clean:
	rm -f main

这个封装的关键思路是:

  • std::function<void()> 接收任意可调用对象
  • static Routine 作为统一入口
  • this 通过arg传入
  • Start负责创建线程
  • Join负责等待回收
  • EnableDetach设置分离状态

六、底层原理:glibc-NPTL库与内核交互

6.1 pthread_create 的两大内部工作

很多人以为pthread_create直接进内核,其实不是。在创建真正的LWP之前,glibc NPTL库要先做很多用户态准备工作。

核心有两步:

第一步:allocate_stack

库要先给线程准备栈和TCB。

过程大致是:

  1. 先查线程栈缓存,看有没有之前退出线程留下的可用栈块
  2. 如果有,优先复用,避免频繁mmap/munmap
  3. 如果没有,调用mmap分配一块匿名内存
  4. 这块内存同时承载TCB和线程栈
  5. 设置Guard保护页

【通俗理解】栈缓存就像线程库自己维护的"栈内存池"。线程退出后,栈不一定立刻还给操作系统,而是先放缓存里;新线程来的时候,先从缓存里拿,减少系统调用开销。

第二步:创建内核LWP

栈和TCB准备好后,库会调用:

text 复制代码
create_thread → do_clone → __clone

最终通过__clone这个汇编封装,进入内核clone系统调用,创建真正的轻量级进程。

所以,pthread_create并不是"单纯调一个系统调用",而是:

text 复制代码
用户调用 pthread_create
  ↓
用户态准备栈和TCB
  ↓
设置线程入口和上下文
  ↓
调用 clone 系统调用
  ↓
内核创建 LWP

6.2 clone 系统调用与关键flags

clone是Linux中非常重要的系统调用,fork和线程创建都建立在它之上。

它和普通函数不同,不仅创建新执行流,还通过flags决定父子任务之间哪些资源共享、哪些复制。

线程创建常用flags
flag 作用
CLONE_VM 共享虚拟地址空间,是线程最核心标志
CLONE_FS 共享文件系统信息,如当前工作目录、umask
CLONE_FILES 共享文件描述符表
CLONE_SIGHAND 共享信号处理方式
CLONE_SETTLS 设置线程局部存储
CLONE_PARENT_SETTID 内核把新LWP的ID写回父进程
CLONE_CHILD_CLEARTID 线程退出时,内核清空tid,用于join等待

【面试思考】为什么CLONE_VM是线程最核心标志?

因为进程和线程最大的区别之一,就在于是否共享虚拟地址空间。如果没有CLONE_VM,父子任务有独立地址空间,那更接近普通进程;加上CLONE_VM并共享其他资源,才更接近线程。

6.3 TCB struct pthread 关键成员

glibc NPTL中的TCB非常复杂,但面试不需要背完整结构体,重点掌握以下字段即可:

字段 作用
tid 内核LWP ID
joinid 标记线程是否处于分离状态,通过IS_DETACHED判断
result 保存线程退出返回值,pthread_join从此读取
start_routine 用户传入的线程入口函数
arg 用户传入的入口参数
stackblock 子线程栈内存起始地址

【通俗理解】TCB就是pthread库在用户态给线程建立的"档案袋"。线程ID、返回值、栈地址、入口函数,都存在这个档案袋里。内核不看这个档案袋,内核只看task_struct和LWP。

6.4 fork 与 clone 的简单对比

操作 是否共享地址空间 典型用途
fork 不共享,子进程独立复制 创建新进程
clone + CLONE_VM 共享虚拟地址空间 创建线程

clone的本质更像一个"通用任务创建接口"。你传不同flags,内核就按不同语义创建新执行流。

clone简单示例
c 复制代码
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

#define STACK_SIZE (1024 * 1024)

static int child_func(void* arg)
{
    printf("child LWP running, getpid() = %d\n", getpid());
    return 0;
}

int main(void)
{
    char* stack = (char*)malloc(STACK_SIZE);
    if (stack == NULL)
    {
        perror("malloc");
        return 1;
    }

    pid_t pid = clone(child_func,
                      stack + STACK_SIZE,
                      CLONE_VM | SIGCHLD,
                      NULL);
    if (pid == -1)
    {
        perror("clone");
        free(stack);
        return 1;
    }

    printf("parent pid = %d, child pid = %d\n", getpid(), pid);
    waitpid(pid, NULL, 0);
    free(stack);

    return 0;
}

编译命令:

bash 复制代码
gcc clone_demo.c -o clone_demo

这个例子中,CLONE_VM表示父子共享虚拟地址空间,已经非常接近线程语义。


七、下篇面试考点总结

高频问题 核心回答
pthread库编译为什么要加-lpthread 链接libpthread.so动态库,否则会报undefined reference
pthread函数出错为什么不用perror pthread函数通过返回值返回错误码,不修改全局errno,应该用strerror
pthread_t 是什么 NPTL中本质是用户态虚拟地址,指向TCB,只在进程内有效,内核不识别
LWP是什么 内核轻量级进程ID,对应task_struct.tid,是内核调度实体,系统全局唯一
pthread_t 和 LWP 关系 1:1模型,一个用户态pthread线程对应一个内核LWP,但二者不是同一概念
主线程栈和子线程栈区别 主线程栈是进程原生栈,支持动态增长;子线程栈由mmap分配,默认8MB,固定大小,不能动态增长
Guard保护页作用 用mprotect(PROT_NONE)设置栈边界保护页,栈溢出时尽早触发段错误
线程终止方式有几种 return、pthread_exit、pthread_cancel
为什么子线程不能调用exit exit终止整个进程,所有线程一起退出
pthread_join 为什么必要 回收线程退出后的TCB和栈资源,避免僵尸线程和内存泄漏
pthread_join 三种返回结果 return拿到返回指针,pthread_exit拿到退出值,cancel拿到PTHREAD_CANCELED
返回值为什么不能用线程栈局部变量 线程退出后栈空间可能失效,访问会造成野指针
joinable 和 detached 区别 joinable需要join回收;detached退出后自动回收,二者互斥
分离后还能join吗 不能,会返回错误
C++普通成员函数为什么不能直接做线程入口 普通成员函数隐式携带this,签名不匹配;应使用static成员函数
pthread_setname_np 的_np 含义 non-portable,非可移植,仅Linux可用
pthread_create 底层做了什么 用户态分配栈和TCB,可能复用栈缓存,然后调用clone创建LWP
clone 最重要的线程标志是什么 CLONE_VM,共享虚拟地址空间
TCB中joinid和result作用 joinid判断是否分离,result保存线程返回值

相关推荐
元岳数字人小元1 小时前
数字人私有化部署:企业级AI数字场景长效落地优选方案
运维·人工智能·开源·人机交互·交互
Thneonl2 小时前
全集群钟差 300 毫秒会发生什么:证书悄悄过期,日志倒流
运维·后端
是jin奥2 小时前
Ubuntu 访问 Windows 共享目录
linux·运维·ubuntu
Gauss松鼠会2 小时前
【GaussDB】破除gaussdb ugin索引支持中文模糊查询的迷思-字符序
java·运维·服务器·网络·数据库·gaussdb·经验总结
科技象限2 小时前
数据防泄密系统怎么选?看它怎么把几道防线叠成网
运维·安全·网络安全·安企神
Mr_韩3 小时前
子网路由实战:无需公网 IP,异地直接访问家庭内网全部设备
运维·网络·物联网·nas
星恒随风3 小时前
Linux基础IO万字详解:从文件描述符fd到重定向、FILE缓冲机制及MiniShell升级
linux·运维·笔记·学习·状态模式
yunwei373 小时前
eBPF 入门开发实践教程一:Hello World,基本框架和开发流程
linux·后端·性能优化
小雪崩3 小时前
嵌入式学习 day65:Linux驱动进阶——pinctrl 子系统和 platform 驱动框架
linux·学习·驱动