【Linux系统:多线程】线程概念与控制

🎬 个人主页艾莉丝努力练剑
专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录
Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬 艾莉丝的简介:


文章目录

  • [1 ~> Linux 线程概念](#1 ~> Linux 线程概念)
    • [1.1 线程的深度定义与内核实现](#1.1 线程的深度定义与内核实现)
    • [1.2 内存管理的底层:分页式存储](#1.2 内存管理的底层:分页式存储)
      • [1.2.1 物理内存的页框管理](#1.2.1 物理内存的页框管理)
      • [1.2.2 虚拟地址到物理地址的 10-10-12 转换](#1.2.2 虚拟地址到物理地址的 10-10-12 转换)
      • [1.2.3 补充:MMU 的权限过滤机制](#1.2.3 补充:MMU 的权限过滤机制)
    • [1.3 线程切换的硬件优势](#1.3 线程切换的硬件优势)
  • [2 ~> 进程与线程的资源边界](#2 ~> 进程与线程的资源边界)
    • [2.1 线程私有资源(不可共享)](#2.1 线程私有资源(不可共享))
    • [2.2 线程共享资源](#2.2 线程共享资源)
  • [3 ~> 线程控制实战(Pthread 库)](#3 ~> 线程控制实战(Pthread 库))
    • [3.1 线程创建:pthread_create](#3.1 线程创建:pthread_create)
    • [3.2 线程终止](#3.2 线程终止)
      • [3.2.1 线程终止的三种方式](#3.2.1 线程终止的三种方式)
      • [3.2.2 注意](#3.2.2 注意)
    • [3.3 线程等待与资源回收](#3.3 线程等待与资源回收)
    • [3.4 补充](#3.4 补充)
      • [3.4.1 补充:PTHREAD_CANCELED 的本质](#3.4.1 补充:PTHREAD_CANCELED 的本质)
      • [3.4.2 线程退出的内存陷阱](#3.4.2 线程退出的内存陷阱)
  • [4 ~> 线程 ID 的本质与内存布局(线程库与内核的映射关系)](#4 ~> 线程 ID 的本质与内存布局(线程库与内核的映射关系))
    • [4.1 库级别的线程标识 pthread_t](#4.1 库级别的线程标识 pthread_t)
    • [4.2 LWP 与 pthread_t 的对应关系](#4.2 LWP 与 pthread_t 的对应关系)
    • [4.3 LWP 与 pthread_t](#4.3 LWP 与 pthread_t)
    • [4.4 线程局部存储(TLS)](#4.4 线程局部存储(TLS))
  • [5 ~> 线程封装的工程实践](#5 ~> 线程封装的工程实践)
    • [5.1 面向对象封装思路](#5.1 面向对象封装思路)
    • [5.2 封装代码片段](#5.2 封装代码片段)
  • [6 ~> 核心补充:clone 系统调用](#6 ~> 核心补充:clone 系统调用)
  • 结尾

1 ~> Linux 线程概念

1.1 线程的深度定义与内核实现

(1) 在 Linux 内核中,线程被称为轻量级进程(LWP, Light Weight Process)。

(2) 传统的进程模型中,一个进程对应一个地址空间,对应一个 task_struct;但在 Linux 线程模型中,多个 task_struct 指向同一个 mm_struct(进程地址空间)。

(3) 线程是"一个进程内部的控制序列",它在进程的虚拟地址空间内运行。

(4) CPU 在进行调度时,并不区分进程和线程,它只认 task_struct。因为线程的 task_struct 共享了大量的进程资源,所以其创建、切换、销毁的开销远小于传统意义上的进程。

(5) 进程是承担分配系统资源的基本实体,而线程是 CPU 调度的基本单位。

1.2 内存管理的底层:分页式存储

1.2.1 物理内存的页框管理

(1) 操作系统将物理内存划分为固定大小的块,称为页框(Page Frame),通常大小为 4KB。

(2) 内核使用 struct page 数组来管理所有物理页。这是一个联合体结构,用于减少内存占用。

(3) _mapcount 成员记录该物理页被多少个页表项映射,是内存回收的关键引用计数。

(4) flags 描述页的状态,如是否为脏页(Dirty)、是否被锁定(Locked)。

1.2.2 虚拟地址到物理地址的 10-10-12 转换

(1) 在 32 位环境下,虚拟地址被逻辑上划分为三部分,用于多级页表索引。

(2) 页目录索引(高 10 位):从 CR3 寄存器获取页目录基地址,通过这 10 位找到对应的页目录项(PDE)。

(3) 页表索引(中间 10 位):PDE 指向一个具体的页表,通过这 10 位找到页表项(PTE)。

(4) 页内偏移(低 12 位):PTE 存储物理页框的起始地址,加上这 12 位偏移量(2^12 = 4KB),精确定位物理内存字节。

1.2.3 补充:MMU 的权限过滤机制

1.3 线程切换的硬件优势

(1) 进程切换开销大:需要切换页表、刷新 TLB、刷新处理器的 L1/L2/L3 Cache。

(2) 线程切换开销小:由于共享同一个页表,TLB 缓存的大部分转换映射依然有效。

(3) 缓存热度:线程切换时,CPU 的 Cache 中缓存的数据和指令依然具有高度的重合性,这使得线程在切换后能迅速进入高频执行状态,不会产生进程切换时的"性能塌陷"。


2 ~> 进程与线程的资源边界

2.1 线程私有资源(不可共享)

(1) 线程 ID:在进程内部唯一标识该执行流。

(2) 寄存器组:包含 PC 指针、栈指针及通用寄存器,保存线程当前的执行上下文。

(3) 独立栈空间:每个线程必须拥有独立的函数调用栈,以维护各自的局部变量和调用关系。

(4) errno:每个线程拥有独立的错误码副本,防止多线程环境下错误信息被覆盖。

(5) 信号屏蔽字:各线程可以对不同的信号进行屏蔽。

2.2 线程共享资源

(1) 代码段与数据段:所有线程共享全局变量、静态变量。

(2) 文件描述符表:一个线程打开文件,其他线程可以直接使用该 FD。

(3) 信号处理方式:若某一线程修改了 SIGINT 的 handler,整个进程的行为都会改变。


3 ~> 线程控制实战(Pthread 库)

3.1 线程创建:pthread_create

(1) 线程库是一个用户态库(NPTL),必须通过 -lpthread 进行手动链接。

(2) pthread_create 的第四个参数 void *arg 是万能指针,可以传递基本类型,也可以传递结构体/类对象地址。

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* thread_routine(void* args) {
    char* msg = (char*)args;
    while (true) {
        std::cout << "Sub thread: " << msg << " PID: " << getpid() << std::endl;
        sleep(1);
    }
}

int main() {
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_routine, (void*)"Hello Thread");
    while (true) {
        std::cout << "Main thread running..." << std::endl;
        sleep(1);
    }
    return 0;
}

3.2 线程终止

3.2.1 线程终止的三种方式

(1)线程函数 return。

(2)调用 pthread_exit(void *value_ptr),注意 value_ptr 不能指向栈上的局部变量。

(3)调用 pthread_cancel(pthread_t thread) 取消目标线程。

3.2.2 注意

在任何线程中调用 exit 都会导致整个进程退出。

3.3 线程等待与资源回收

(1) pthread_join:主线程必须调用此函数回收子线程资源,否则会产生类似"僵尸进程"的残留问题。

(2) 返回值获取 :通过 void **retval 参数,可以获取线程函数 return 的结果或 pthread_exit 传出的数据。

(3) pthread_cancel :用于取消正在运行的线程。被取消的线程其退出码固定为宏 PTHREAD_CANCELED(即 -1)。

3.4 补充

3.4.1 补充:PTHREAD_CANCELED 的本质

当一个线程是被 pthread_cancel 异常取消退出的,通过 pthread_join 获取到的退出码是一个宏定义 PTHREAD_CANCELED,在大多数系统中,其数值为 -1(即 (void*)-1)。

3.4.2 线程退出的内存陷阱

在使用 pthread_exitreturn 返回数据给主线程时,严禁返回局部变量的地址 。由于子线程栈在退出后会被销毁或重用,主线程拿到的指针将指向野内存。建议使用全局变量、静态变量或 malloc 申请的堆空间。


4 ~> 线程 ID 的本质与内存布局(线程库与内核的映射关系)

4.1 库级别的线程标识 pthread_t

(1) 线程库通过 mmap 在进程地址空间的共享区为每个线程分配了一块内存,这块内存被称为线程控制块(TCB)。

(2) pthread_t 的数值本质上就是这块 TCB 在共享区内的起始地址。

(3) 子线程的栈也是在这块 mmap 出来的空间内。

4.2 LWP 与 pthread_t 的对应关系

(1) LWP 是内核标识,用于 OS 调度,全局唯一。

(2) pthread_t 是库标识,仅在进程内部有意义。

(3) 程序员在代码中使用 pthread_t 进行管理,而内核通过映射关系找到对应的 LWP 进行物理调度。

4.3 LWP 与 pthread_t

(1) LWP(Light Weight Process):内核层面的线程 ID,由系统分配。在终端使用 ps -aL 可以查看。

(2) pthread_t:用户态线程库(NPTL)层面的 ID。

(3) 映射关系:NPTL 库在 mmap 区域(共享区)为每个线程开辟了一块空间。pthread_t 的值实际上就是这块内存空间的起始地址。

4.4 线程局部存储(TLS)

(1) 线程库在共享区中为每个线程维护了线程描述符(TCB)、局部变量和独立的栈。

(2) 在 C/C++ 代码中,使用 __thread 关键字修饰全局变量,可以使每个线程拥有一份该变量的私有副本,互不干扰。


5 ~> 线程封装的工程实践

5.1 面向对象封装思路

(1) 在 C++ 中封装线程类,可以将线程的逻辑与资源管理解耦。

(2) 必须处理 this 指针问题:pthread_create 需要一个 void* (*)(void*) 类型的静态函数,而类成员函数隐含了 this 指针作为第一个参数,因此需要将回调函数设为 static,并将类对象的 this 指针作为参数传入。

5.2 封装代码片段

cpp 复制代码
class Thread {
public:
    Thread(std::function<void()> func) : _func(func) {}
    
    static void* start_routine(void* args) {
        Thread* t = static_cast<Thread*>(args);
        t->_func(); // 执行任务
        return nullptr;
    }

    void run() {
        pthread_create(&_tid, nullptr, start_routine, this);
    }

    void join() {
        pthread_join(_tid, nullptr);
    }
private:
    pthread_t _tid;
    std::function<void()> _func;
};

6 ~> 核心补充:clone 系统调用

(1) Linux 下所有的线程创建最终都会调用内核的 clone 系统调用。

(2) 通过传入不同的 flags 掩码(如 CLONE_VMCLONE_FILESCLONE_FS),clone 可以灵活控制父子执行流之间资源的共享程度。

(3) 如果不传这些共享标志位,clone 的行为就退化成了 fork

对比:进程 vs 线程资源共享表

补充:线程与进程对信号的处理差异


结尾

uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!" "技术之路难免有困惑,但同行的人会让前进更有方向。" |

结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!

往期回顾

【Linux系统:多线程】Linux 内核与多线程深度强化干货25条

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
linux开发之路2 小时前
C++实现Whisper+Kimi端到端AI智能语音助手
c++·人工智能·llm·whisper·openai
IMPYLH2 小时前
Linux 的 mkfifo 命令
linux·运维·服务器·bash
CHS_Lab2 小时前
DELL服务器阵列崩溃恢复方法
服务器·数据恢复·dell·raid·阵列恢复·戴尔恢复·服务器恢复
AIminminHu2 小时前
OpenGL渲染与几何内核那点事-项目实践理论补充(二-1-(2):当你的CAD学会“听话”:从鼠标点击到自然语言命令)
c++·人工智能
徒 花2 小时前
Python知识学习03
开发语言·python·学习
喝醉的小喵2 小时前
iptables 规则重启机器后丢失导致k8s网络不可用
网络·后端·容器·kubernetes·虚拟化
M1nat0_2 小时前
Linux 进程信号:从生活类比到内核原理
linux·运维
jinglong.zha2 小时前
AScript + Cursor:让 AI 直接操控你的设备,一句话完成自动化编程(源代码)
运维·人工智能·自动化·ascript·openclaw
运维行者_2 小时前
MSP网络管理破局者:IPAM+SPM插件终结IP冲突与安全威胁
运维·服务器·开发语言·网络·安全·web安全·php