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

张开发
2026/5/18 22:32:05 15 分钟阅读
【Linux系统:多线程】线程概念与控制
个人主页艾莉丝努力练剑❄专栏传送门《C语言》《数据结构与算法》《C/C干货分享学习过程记录》《Linux操作系统编程详解》《笔试/面试常见算法从基础到进阶》《Python干货分享》⭐️为天地立心为生民立命为往圣继绝学为万世开太平 艾莉丝的简介文章目录1 ~ Linux 线程概念1.1 线程的深度定义与内核实现1.2 内存管理的底层分页式存储1.2.1 物理内存的页框管理1.2.2 虚拟地址到物理地址的 10-10-12 转换1.2.3 补充MMU 的权限过滤机制1.3 线程切换的硬件优势2 ~ 进程与线程的资源边界2.1 线程私有资源不可共享2.2 线程共享资源3 ~ 线程控制实战Pthread 库3.1 线程创建pthread_create3.2 线程终止3.2.1 线程终止的三种方式3.2.2 注意3.3 线程等待与资源回收3.4 补充3.4.1 补充PTHREAD_CANCELED 的本质3.4.2 线程退出的内存陷阱4 ~ 线程 ID 的本质与内存布局线程库与内核的映射关系4.1 库级别的线程标识 pthread_t4.2 LWP 与 pthread_t 的对应关系4.3 LWP 与 pthread_t4.4 线程局部存储TLS5 ~ 线程封装的工程实践5.1 面向对象封装思路5.2 封装代码片段6 ~ 核心补充clone 系统调用对比进程 vs 线程资源共享表补充线程与进程对信号的处理差异结尾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是万能指针可以传递基本类型也可以传递结构体/类对象地址。#includeiostream#includepthread.h#includeunistd.hvoid*thread_routine(void*args){char*msg(char*)args;while(true){std::coutSub thread: msg PID: getpid()std::endl;sleep(1);}}intmain(){pthread_t tid;pthread_create(tid,nullptr,thread_routine,(void*)Hello Thread);while(true){std::coutMain thread running...std::endl;sleep(1);}return0;}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_exit或return返回数据给主线程时严禁返回局部变量的地址。由于子线程栈在退出后会被销毁或重用主线程拿到的指针将指向野内存。建议使用全局变量、静态变量或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) LWPLight 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 封装代码片段classThread{public:Thread(std::functionvoid()func):_func(func){}staticvoid*start_routine(void*args){Thread*tstatic_castThread*(args);t-_func();// 执行任务returnnullptr;}voidrun(){pthread_create(_tid,nullptr,start_routine,this);}voidjoin(){pthread_join(_tid,nullptr);}private:pthread_t _tid;std::functionvoid()_func;};6 ~ 核心补充clone 系统调用(1) Linux 下所有的线程创建最终都会调用内核的clone系统调用。(2) 通过传入不同的flags掩码如CLONE_VMCLONE_FILESCLONE_FSclone可以灵活控制父子执行流之间资源的共享程度。(3) 如果不传这些共享标志位clone的行为就退化成了fork。对比进程 vs 线程资源共享表补充线程与进程对信号的处理差异结尾uu们本文的内容到这里就全部结束了艾莉丝在这里再次感谢您的阅读艾莉丝努力练剑C/C Linux 底层探索者 | 一个正在努力练剑的技术博主【关注】跟随我一起深耕技术领域见证每一次成长。❤️【点赞】让优质内容被更多人看见让知识传递更有力量。⭐【收藏】把核心知识点存好在需要时随时查、随时用。【评论】分享你的经验或疑问评论区一起交流避坑不要忘记给博主“一键四连”哦“今日练剑达成”“技术之路难免有困惑但同行的人会让前进更有方向。”结语希望对学习Linux相关内容的uu有所帮助不要忘记给博主“一键四连”哦往期回顾【Linux系统多线程】Linux 内核与多线程深度强化干货25条博主在这里放了一只小狗大家看完了摸摸小狗放松一下吧૮₍ ˶ ˊ ᴥ ˋ˶₎ა

更多文章