Re:Linux系统篇(五十二)线程篇 · 五:直击 Linux 内核底层:TCB 与 LWP 一体两面全解析(附自定义线程封装)


◆ 博主名称: 小此方-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 的映射关系协同工作:

  1. 用户层视角 :开发者调用 pthread_create() 函数创建线程,库便会在动态库映射区中开辟空间并实例化一个 struct tcb
  2. 内核层视角 :库通过系统调用(如 clone())向内核申请创建一个新的轻量级进程(LWP),该 LWP 拥有独立的 task_struct 并共享父进程的资源。
  3. 协作调度:内核调度器直接调度的是 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 库的内部结构中,每个线程管理单元是连续分配的一整块内存空间。这块空间包含三部分:

  1. struct pthread(TCB 线程控制块):保存线程的描述属性。
  2. 线程局部存储(TLS,Thread Local Storage) :用于存储被 __thread 修饰的线程私有全局/静态变量。
  3. 线程栈(Thread Stack):该新线程独占的栈空间,用于存放其局部变量和函数调用栈帧。

  (示意图,真实情况不一定挨在一起)

3.1.1 线程 ID(pthread_t tid)的本质

  调用 pthread_create(&tid, ...) 获取到的线程 ID(如 pthread_t tid),本质上就是该线程对应的管理块(TCB / struct pthread)在当前进程虚拟地址空间中的起始地址

3.2整个线程的生命周期

  1. 代码段调用pthread_create函数创建线程块。
  2. 线程调用代码段的线程执行函数。
  3. 执行返回return (void*) xxx;返回值写入 struct pthread中的void* ret中。
  4. 代码段执行join,调用tid指针指向的那个线程块。传入输出型参数ret获取struct pthread中存储的ret。
  5. 释放线程块。

  为什么线程运行结束后用系统命令(如 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, ...);
  1. 确定函数入口与栈地址pthread 库先在用户空间的 mmap 区域开辟包含 struct pthread、TLS 和线程栈的连续内存块。随后将执行函数入口 fn(即 routine)和刚分配好的用户栈地址 stack 作为参数传递给 clone
  2. 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_forkkernel_clone )。

  用户态 API 在底层映射的常见三种创建方式:

  1. fork():创建子进程,默认不共享地址空间,采用**写时复制(Copy-on-Write)**机制。
  2. vfork() :创建子进程,父进程会被挂起,子进程与父进程共享地址空间,直到子进程调用 execexit
  3. clone() :创建线程或定制化进程,通过传入不同的 flags 标志位(如 CLONE_VMCLONE_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 【此方的技术栈】。

相关推荐
ltl9 小时前
校验和与数据完整性:CRC32C、xxHash 与端到端校验
linux
我是小灰灰吖9 小时前
Ubuntu 22.04 设置 CPU 性能模式:提升系统响应速度
linux·ubuntu
ltl10 小时前
持续性能分析:Parca、Pyroscope、Grafana Beyla
linux
ltl10 小时前
Direct I/O 与 O_DIRECT:绕过缓存的得与失
linux
盐焗鹌鹑蛋11 小时前
【Linux】进程控制
linux
微三云 - 廖会灵 (私域系统开发)11 小时前
抖店 OPC 自动化运营系统架构与商业代理分控体系完整解析
运维·系统架构·自动化
办公室马主任12 小时前
华南机械加工企业选MES服务商怎么选?
大数据·运维·人工智能·制造
盐焗鹌鹑蛋12 小时前
【Linux】进程程序替换
linux
鸠摩智首席音效师12 小时前
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead
linux