Linux NPTL线程:创建/终止/等待/分离/栈布局内核原理|线程下篇
-
- 前言
- [三、Linux线程控制 --- POSIX pthread库实操](#三、Linux线程控制 — POSIX pthread库实操)
-
- [3.1 pthread库基础与错误处理](#3.1 pthread库基础与错误处理)
- [3.2 线程创建 `pthread_create`](#3.2 线程创建
pthread_create) - [3.3 线程的三种终止方式](#3.3 线程的三种终止方式)
-
- [方式一:线程入口函数 `return`](#方式一:线程入口函数
return) - 方式二:`pthread_exit`
- 方式三:`pthread_cancel`
- 三种终止方式完整示例
- [方式一:线程入口函数 `return`](#方式一:线程入口函数
- [3.4 线程等待 `pthread_join`](#3.4 线程等待
pthread_join) - [3.5 线程分离 `pthread_detach`](#3.5 线程分离
pthread_detach)
- 四、线程ID及进程虚拟地址栈布局
-
- [4.1 `pthread_t` 与内核LWP的区别](#4.1
pthread_t与内核LWP的区别) -
- [`pthread_t` 是什么?](#
pthread_t是什么?) - LWP是什么?
- [`pthread_self()` 和 `ps -aL` 分别拿到什么?](#
pthread_self()和ps -aL分别拿到什么?)
- [`pthread_t` 是什么?](#
- [4.2 主线程栈、子线程栈与Guard警戒保护页](#4.2 主线程栈、子线程栈与Guard警戒保护页)
- [4.1 `pthread_t` 与内核LWP的区别](#4.1
- 五、C++面向对象封装线程
- 六、底层原理:glibc-NPTL库与内核交互
-
- [6.1 `pthread_create` 的两大内部工作](#6.1
pthread_create的两大内部工作) - [6.2 `clone` 系统调用与关键flags](#6.2
clone系统调用与关键flags) - [6.3 TCB `struct pthread` 关键成员](#6.3 TCB
struct pthread关键成员) - [6.4 `fork` 与 `clone` 的简单对比](#6.4
fork与clone的简单对比)
- [6.1 `pthread_create` 的两大内部工作](#6.1
- 七、下篇面试考点总结
前言
在上篇中,我们已经建立了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);
四个参数逐一看:
thread:输出参数,用于保存新创建线程的用户态IDpthread_tattr:线程属性结构体,传NULL表示使用默认属性start_routine:线程入口函数,签名必须是void* (*)(void*)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:进程IDLWP:轻量级进程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:目标线程IDvalue_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);
两种分离方式
- 其他线程分离目标线程:
c
pthread_detach(tid);
- 线程自己分离自己:
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_tps -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。
过程大致是:
- 先查线程栈缓存,看有没有之前退出线程留下的可用栈块
- 如果有,优先复用,避免频繁
mmap/munmap - 如果没有,调用
mmap分配一块匿名内存 - 这块内存同时承载TCB和线程栈
- 设置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保存线程返回值 |