
◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)
文章目录
- 概要&序論
- 一、线程的一体两面:TCB与LWP
-
- [1.1 动态库映射与进程地址空间](#1.1 动态库映射与进程地址空间)
- [1.2 用户级线程的管理机制(先描述,再组织)](#1.2 用户级线程的管理机制(先描述,再组织))
-
- [1.2.1 线程控制块 struct tcb](#1.2.1 线程控制块 struct tcb)
- [1.2.2 用户级 TCB 与内核 LWP 的分工](#1.2.2 用户级 TCB 与内核 LWP 的分工)
- 二、线程ID到底是什么
- [三、线程栈的存储原理与 LWP-TCB 联动机制](#三、线程栈的存储原理与 LWP-TCB 联动机制)
-
- [3.1 线程栈的分布与存储原理](#3.1 线程栈的分布与存储原理)
-
- [3.1.1 线程 ID(pthread_t tid)的本质](#3.1.1 线程 ID(pthread_t tid)的本质)
- 3.2整个线程的生命周期
- [3.3 LWP 与 TCB 的联动机制](#3.3 LWP 与 TCB 的联动机制)
-
- [3.3.1 底层创建流程:clone 系统调用](#3.3.1 底层创建流程:clone 系统调用)
- [3.3.2 "带饭"比喻理解用户级与内核级职责](#3.3.2 “带饭”比喻理解用户级与内核级职责)
- [3.4 核心补充与延伸机制](#3.4 核心补充与延伸机制)
-
- [3.4.1 主线程与新线程的执行顺序](#3.4.1 主线程与新线程的执行顺序)
- [3.4.2 线程分离(pthread_detach)的原理](#3.4.2 线程分离(pthread_detach)的原理)
- [3.4.3 内核底层的统一创建入口(do_fork)](#3.4.3 内核底层的统一创建入口(do_fork))
- 3.5源码分析
- 二、封装一个线程
概要&序論
Hello大家好,我是此方,本文深入剖析 Linux 线程底层实现机制,涵盖进程地址空间映射、用户级 TCB 与内核级 LWP 的分工联动,拆解线程栈存储原理与线程 ID 的本质,并通过 clone/do_fork 源码分析与线程封装实战完成全流程讲解。
一、线程的一体两面:TCB与LWP
Linux 本身没有真正的内核级线程,而是用轻量级进程(LWP)来模拟实现的。因此,操作系统层面的系统调用接口并不直接提供线程接口;而是在用户层对轻量级进程进行了二次封装,从而形成了用户级别的原生线程库pthread。

1.1 动态库映射与进程地址空间
原生线程库 libpthread.so 本质上是一个动态库(ELF 格式)。当可执行程序启动并加载时,系统通过动态链接和动态地址重定向,将 pthread 库从磁盘加载到物理内存中,并将其映射到当前进程的共享区。

1.2 用户级线程的管理机制(先描述,再组织)
既然 Linux 内核层面只有轻量级进程(LWP),那么"线程"的概念及相关控制结构就必须完全由用户层的 pthread 动态库来进行维护和管理。库内部会同时存在多个被创建好的线程,因此库必须对这些线程进行统一管理。
有些人想:线程怎么会被库管理呢
理解"线程被库管理"其实非常简单,就像我们使用 C 标准库的
fopen函数一样:
- 文件管理 :调用
fopen时,C 标准库会在堆内存中为你创建一个FILE结构体 (封装了文件描述符、缓冲区等),用户拿到的是一个FILE*指针。- 线程管理 :调用
pthread_create时,pthread线程库会在进程的共享区中为你创建一个struct tcb(线程控制块) ,用户拿到的pthread_t(线程 ID)本质上就是该 TCB 在地址空间中的首地址。
1.2.1 线程控制块 struct tcb
为了管理线程,动态库采用了先描述,再组织 的思想,为每一个用户级线程定义了专门的数据结构,通常被称为 TCB(Thread Control Block,线程控制块) 或 struct pthread。
TCB 中主要维护了线程独立拥有的属性:
- 线程 ID :用于唯一标识用户层线程(如
pthread_t类型,实质上是该线程在动态库映射区中的首地址)。 - 线程状态与分离属性 :包含线程的运行状态以及标识线程是否可被等待的属性(如
int joinable)。 - 线程独立的栈结构 :由于所有线程共享进程地址空间的栈区,为了避免栈冲突,每个用户线程在动态库的映射区中都会分配一块独立的内存,作为该线程的用户栈。
- 线程栈大小:记录当前线程栈空间的分配上限。
1.2.2 用户级 TCB 与内核 LWP 的分工
整个线程的概念,一部分在用户层实现,一部分在内核实现。
需要明确的是,TCB 只负责维护用户层面的线程属性,而与底层的调度息息相关的属性(如优先级、时间片、上下文信息 等)并不由 TCB 管理,而是由内核层的 LWP的 PCB 统一维护。
用户级线程与内核级轻量级进程之间通过 1:1 的映射关系协同工作:
- 用户层视角 :开发者调用
pthread_create()函数创建线程,库便会在动态库映射区中开辟空间并实例化一个struct tcb。 - 内核层视角 :库通过系统调用(如
clone())向内核申请创建一个新的轻量级进程(LWP),该 LWP 拥有独立的task_struct并共享父进程的资源。 - 协作调度:内核调度器直接调度的是 LWP,而用户通过线程 ID(TCB 的起始地址)来操控和管理对应的线程状态。
二、线程ID到底是什么
- pthread_create函数会产生一个线程ID,存在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
- 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
- pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
- 线程库提供了pthread_self函数,可以获得线程自身的ID:
c
pthread_t pthread_self(void);
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
三、线程栈的存储原理与 LWP-TCB 联动机制
3.1 线程栈的分布与存储原理
我们上面讲了,线程有独立的栈结构,以确保函数调用、局部变量分配和上下文压栈时互不干扰。那么什么是独立的栈结构呢?
- 主线程栈(Main Thread Stack):主线程直接使用进程地址空间中传统的"栈区"(位于高地址向下增长的区域)。
- 新线程栈(新创建的线程栈) :通过
pthread_create()创建的新线程,其栈空间并不 使用进程原生的栈区,而是在动态库映射区(mmap 区域) 中开辟。
------对,这很反直觉。跟你以前学的东西不太一样。
在 pthread 库的内部结构中,每个线程管理单元是连续分配的一整块内存空间。这块空间包含三部分:
struct pthread(TCB 线程控制块):保存线程的描述属性。- 线程局部存储(TLS,Thread Local Storage) :用于存储被
__thread修饰的线程私有全局/静态变量。 - 线程栈(Thread Stack):该新线程独占的栈空间,用于存放其局部变量和函数调用栈帧。

(示意图,真实情况不一定挨在一起)
3.1.1 线程 ID(pthread_t tid)的本质
调用 pthread_create(&tid, ...) 获取到的线程 ID(如 pthread_t tid),本质上就是该线程对应的管理块(TCB / struct pthread)在当前进程虚拟地址空间中的起始地址。
3.2整个线程的生命周期
- 代码段调用pthread_create函数创建线程块。
- 线程调用代码段的线程执行函数。
- 执行返回return (void*) xxx;返回值写入 struct pthread中的void* ret中。
- 代码段执行join,调用tid指针指向的那个线程块。传入输出型参数ret获取struct pthread中存储的ret。
- 释放线程块。
为什么线程运行结束后用系统命令(如 ps -L)再也查询不到该线程:线程结束的时候我们操作系统内核中的LWP自动就释放了。 但是库里面的东西还是没有被释放,于是PS无法查询到。
以上,所以资源为什么会泄漏为什么获取ret要使用二级指针我们就都知道了。
3.3 LWP 与 TCB 的联动机制
Linux 系统下的用户级线程与内核级 LWP(Light Weight Process,轻量级进程) 遵循 1:1 模型 。用户态的 struct pthread(TCB)与内核态的 task_struct 通过底层系统调用紧密联动。
3.3.1 底层创建流程:clone 系统调用
当在用户态调用 pthread_create 时,库内部会通过 clone 系统调用向内核申请创建轻量级进程:
c
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...);
- 确定函数入口与栈地址 :
pthread库先在用户空间的 mmap 区域开辟包含struct pthread、TLS 和线程栈的连续内存块。随后将执行函数入口fn(即routine)和刚分配好的用户栈地址stack作为参数传递给 clone。 - CPU 上下文绑定 :内核接收到参数后创建对应的
task_struct(LWP)。当 CPU 调度该 LWP 时,会自动将入口地址载入 PC 寄存器,并将栈指针指向用户态开辟的线程栈,使 LWP 知道去哪里读取数据和执行代码。

3.3.2 "带饭"比喻理解用户级与内核级职责
为什么叫用户级线程? 因为线程控制块(struct pthread)的内存空间直接维护在用户区的动态库中。线程的调度信息、时间片等全部交由内核中的 LWP 管理,用户级线程自身无需维护调度逻辑。
- 形象比喻:就像你在宿舍打游戏,让舍友帮忙带一份猪脚饭。你只需要在脑子(用户态 TCB)里记录"时间、人物、带饭内容",随后只需"躺平"等待;真正出门跑腿(消耗 CPU 时间片执行任务)的是舍友(内核 LWP)。
- 返回值传递 :LWP 执行完
routine函数后,将最终结果写入用户态struct pthread中的void *ret变量中,最后内核 LWP 自动销毁退出。
3.4 核心补充与延伸机制
3.4.1 主线程与新线程的执行顺序
调用 pthread_create 成功后,主线程与新创建的线程谁先运行是不确定的,这完全取决于内核调度器的策略和当前 CPU 的负载情况。
3.4.2 线程分离(pthread_detach)的原理
用户级线程结构体中维护着一个属性字段 joinable(默认值为 1)。
- if (joinable == 1) :默认处于可结合状态。线程退出时内核 LWP 自动释放,但用户态 TCB 块仍保留,直到主线程调用 pthread_join 读取其返回值后才会被完全释放(否则会造成用户态内存泄漏)。
- if (joinable == 0) :设置分离(调用 pthread_detach)后,该字段变为 0。线程退出时,不再等待主线程 join,会自动清理并释放用户态的线程控制块。
所有线程都在一个虚拟地址空间的共享区当中,但是每个线程只能拿到自己线程控制块的虚拟地址。
3.4.3 内核底层的统一创建入口(do_fork)
在 glibc 中,创建线程在系统调用层面使用的是 clone (或现代内核的 clone3 )。但在 Linux 内核底层,无论是创建线程还是进程,最终在内核中都会汇聚到同一个核心函数:do_fork (较新的内核版本中称为 _do_fork 或 kernel_clone )。
用户态 API 在底层映射的常见三种创建方式:
- fork():创建子进程,默认不共享地址空间,采用**写时复制(Copy-on-Write)**机制。
- vfork() :创建子进程,父进程会被挂起,子进程与父进程共享地址空间,直到子进程调用 exec 或 exit。
- clone() :创建线程或定制化进程,通过传入不同的 flags 标志位(如
CLONE_VM、CLONE_FS等)来决定共享哪些资源。
3.5源码分析
以下是 glibc-2.4 中 pthread 源码相关内容:(路径:nptl/pthread_create.c )
看到这个代码样式可能会让人一瞬间有些懵,因为你在日常写 C/C++ 时几乎见不到这种写法。这其实是 K&R 风格(旧标准 C 语言)的函数定义语法。图片中的代码是 Linux 核心 C 库(glibc)中 pthread_create 的底层源码片段。


我们在写 C 语言多线程时,常常会看到这个函数的原型:
c
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
这里的第二个参数 attr,在底层对应的就是你截图里的这个 struct pthread_attr。它是程序员用来定制 线程特性的(比如你想手动把线程栈改小一点,或者让线程一出生就是分离状态 detach )。

二、封装一个线程
cpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdio>
#include <cstring>
#include <functional>
#include<unistd.h>
namespace MyThread{
uint32_t number = 1;
template <typename T>
class Thread{
using func_t = std::function<void(T)>;
private:
void enablerunning(){
_isrunning = true;
}
void enabledetach(){
_isdetach = true;
}
static void*routine(void* args)
{
Thread<T>* self = (Thread<T>*) args ;
self->_isrunning = true ;
self->_func(self->_data);
return nullptr ;
}
public:
Thread(T data , func_t func)
: _tid(0), _isrunning(false), _isdetach(false), _ret(nullptr), _func(func), _data(data){
_name = "Thread-" + std::to_string(number++);
}
~Thread() {}
bool Start(){
if (_isrunning)
return false;
int n = pthread_create(&_tid, nullptr, routine, this);
if (n == 0){
std::cout << "线程[" << _name << "]创建成功" << std::endl;
return true;
}
else{
std::cout << "线程[" << _name << "]创建失败" << std::endl;
return false;
}
return false;
}
bool Detach(){
if (!_isdetach){
int n = pthread_detach(_tid);
if (n == 0){
std::cout << "线程[" << _name << "]分离成功" << std::endl;
_isdetach = true ;
return true;
}
else{
std::cout << "线程[" << _name << "]分离失败" << std::endl;
return false;
}
}
return false;
}
bool Stop(){
if (_isrunning){
int n = pthread_cancel(_tid);
if (n == 0){
std::cout << "线程[" << _name << "]取消成功" << std::endl;
_isrunning = false;
return true;
}
else{
std::cout << "线程[" << _name << "]取消失败" << std::endl;
return false;
}
}
return false;
}
bool Join(){
if (_isdetach){
std::cout << "线程[" << _name << "]为分离线程,不可被主动回收" << std::endl;
return false;
}
if (_isrunning){
int n = pthread_join(_tid, &_ret);
if (n == 0){
std::cout << "线程[" << _name << "]回收成功" << std::endl;
_isrunning = false;
return true;
}
else{
std::cout << "线程[" << _name << "]回收失败" << std::endl;
return false;
}
}
return false;
}
private:
pthread_t _tid;
bool _isrunning;
bool _isdetach;
void *_ret;
func_t _func;
T _data;
std::string _name;
};
}
cpp
#include "MyThread.hpp"
class ThreadData
{
private:
std::string _name;
pthread_t _tid;
};
int main()
{
ThreadData tda;
MyThread::Thread<ThreadData> thread_1(tda, [](ThreadData td)
{ std::cout << "线程正在执行任务1" << std::endl; sleep(1);});
MyThread::Thread<ThreadData> thread_2(tda, [](ThreadData td)
{ std::cout << "线程正在执行任务2" << std::endl; sleep(1);});
MyThread::Thread<ThreadData> thread_3(tda, [](ThreadData td)
{ std::cout << "线程正在执行任务3" << std::endl; sleep(1);});
MyThread::Thread<ThreadData> thread_4(tda, [](ThreadData td)
{ std::cout << "线程正在执行任务4" << std::endl; sleep(1);});
thread_1.Start();
thread_2.Start();
thread_3.Start();
thread_4.Start();
thread_1.Stop();
thread_2.Stop();
thread_3.Stop();
thread_4.Stop();
thread_1.Join();
thread_2.Join();
thread_3.Join();
thread_4.Join();
return 0;
}
好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye! Linux、C++、算法持续连载中,欢迎关注WeChat Official Account 【此方的技术栈】。