Linux相关概念和易错知识点(28)(线程控制、Linux下线程的底层)
目录
1.线程控制
(1)pthread和thread库
(2)线程的创建、等待和分离
①线程创建
②线程等待
③线程分离
④线程替换(不可行)
(3)线程的终止和取消
①线程终止
②线程取消
2.Linux下线程的底层
(1)线程ID
(2)资源划分和维护
①新线程栈的动态创建和销毁
②struct pthread的存储
③线性局部存储
④线性栈和mmap
(3)clone
(4)创建线程过程
1.线程控制
(1)pthread和thread库
线程的使用需要pthread库,这是用户级别的、通用的、基于 POSIX 标准的线程库。这个库提供C语言接口,并且具备跨平台性。很明显,它基于操作系统底层的线程机制实现并进一步封装,向上提供了统一的接口。
pthread库的实现是glibc的一部分,所以pthread库是Linux系统自带的线程库。但由于线程库可能不是项目中必需的,而且有可能和其它库引发冲突,因此Linux有可能不会默认链接pthread动态库。如果要使用线程,必须编译时选择-lpthread(不需要选择查找目录、头文件位置,系统能找到)。
除此之外,C++11引入了<thread>,这是C++风格的线程实现,同样具有跨平台性。Linux下使用<thread>需要链接原生线程库-lpthread
(2)线程的创建、等待和分离
①线程创建
当创建成功时返回0,失败时返回错误码。
参数我们不关注第二个,默认填nullptr。第一个参数pthread_t是线程的id,用于唯一标识线程;第三个是让线程线程执行的函数(回调函数),该函数返回值为void*,参数为void*;第四个参数是传入该回调函数的参数。
下面是简单的使用
我们可以看到引用线程库后,就算还没有创建线程,主程序的代码执行可被认为是主线程,可以获取它的唯一标识pthread_t。虽然Linux下都是LWP,但在C语言上封装后就出现了线程的概念。
我们可以认为,线程的存在就是为了执行某一个函数,主线程的存在就是为了执行main函数(执行main函数的线程就是主线程),当main函数结束后代表主线程执行完毕,进而表示整个进程的结束。其余线程也都可以这么理解,它们的生命周期从开始执行函数为始,结束执行函数为终。
主线程也可以新建一个线程并让它运行代码。当主线程结束运行后,程序退出。
但这里有个问题:万一主线程比创建的线程更早结束,就会导致创建的线程没有完成任务。那应该怎么保证主线程在最后退出呢?sleep?这太难控制了,而且效率很低。
②线程等待
成功返回0,错误返回错误码。
假如thread1调用该join函数,其第一个参数是thread2,这就意味着thread1会阻塞在这个函数直到thread2结束,第二个参数是thread1接收thread2的返回值所用。
除此之外,join之后线程的资源(如线程的退出状态、栈等)将会被释放。如果不及时join,这些未释放的资源会一直保留在系统中,直到整个进程结束,就像僵尸进程占用系统资源一样。
下面是对刚才代码的修改
这样主线程就会在等待的线程退出后才退出,并且能够获取到子线程的函数返回值。要特别注意子线程的返回值要求为void*,参数为void*,必须完全对应才能编译通过。如果不对子线程的返回值感兴趣,可以传nullptr作为join函数的第二个参数。
线程等待类似回收僵尸进程,只要join对应线程的id,就算该线程已经结束了,也都能获取到该线程的返回值,并且这个返回值需要通过堆区创建空间。对于所有线程来说,堆区是共享的,谁拿着堆的入口地址,谁就能访问空间。
在线程中虽然名义上进程地址空间被划分给了不同线程,但线程之间可以互相访问,只要有地址。
join成功了,代表此刻数据一定处理完了,这样就可以放心执行后面的代码了。
③线程分离
join有个坏处,就是join的线程必须等待被join的线程执行完后才会继续执行代码,但有的时候线程要做自己的事情,比如主线程承担分配任务的使命。想要不阻塞在join函数,则要将目标线程设置为分离状态,即detach状态。
线程分离出对应的线程后join会失败,join的返回值不是0。之后该线程执行完毕后会自动释放空间,而不需要join。同时,除非极端情况,就算主线程走完了,分离的线程还能执行,进程会在分离线程执行完毕后回收,但要注意一些共享资源的访问问题,有可能分离线程会去访问已被释放的线程的资源。
④线程替换(不可行)
线程能不能程序替换?不可以。只有进程可以程序替换,进程替换会把代码数据全部替换,线程没这个能力,它不能对进程做出操作。但我们可以在线程里面fork,进一步实现程序替换。
(3)线程的终止和取消
①线程终止
主线程return表示整个进程结束,此刻所有线程也都会退出,不管是否执行完毕。新线程return表示该线程退出,其余线程不受影响。
但注意,任何线程在任何地方调用exit均表示进程退出,即所有线程都会终止。相对的,例如pthread_exit((void*)10)这种exit退出方式就相对温和,只是结束相应调用的线程并且交出返回值给等待它的线程,pthread_exit和return等价,平时就用return就好。
②线程取消
线程可以被取消,主线程可以在任何时候取消任何线程的执行,这是被动式退出,不由其它线程决定。
其参数就是被取消的线程id。取消成功返回0,失败返回错误码。
取消一个线程的前提是目标线程已经被启动了,即在create函数之后。但一般不推荐取消线程,因为对应线程的状态不清楚,处理的数据状态也不清楚,因此盲目取消会导致程序混乱。
注意,取消的线程依旧需要被join,这就相当于回收僵尸进程,被取消的线程会返回-1,该返回值其实是宏PTHREAD_CANCELED。
2.Linux下线程的底层
(1)线程ID
线程id(即pthread_t)是一个地址,这个地址本身也有唯一性。
pthread.so库加载到内存中,这个库被经过页表映射到共享区。创建线程的其实是在调用库方法,由于进程内的所有线程共享进程地址空间,因此它们都能执行库的其它方法。
Linux中严格上只有LWP,但用户要用线程,想要用线程接口(create、join等),想要线程的属性(线程的id、优先级、状态、栈大小等),怎么办呢?
用户不关心LWP的属性,他们只关心线程的属性,这就是pthread.so库单独提供的。pthread.so库要维护相关属性,要存储相应的结构体来存储相关属性。这和glibc维护FILE一样,所有文件的属性信息都是由glibc库来进行维护的,当要对某个文件进行操作时,都是调用glibc的函数对其维护下的结构体进行修改。注意,这些结构体由动态库维护(调用动态库的函数对这些结构体进行修改),其结构体的物理内存也在库的里面,下面的pthread.so也是如此。
pthread.so会维护该进程下所有线程的属性。对于一个线程来说,其属性的集合就相当于结构体TCB,线程的id、优先级、状态、栈大小、返回值都在里面保存着的。一般来说,所有线程的属性的结构体整体形成的是一个数组。既然线程属性保存在一个物理内存中,那么理所应当的,其线程id的值就是相应结构体的地址,也就是pthread_t类型,pthread_create的第一个参数获取的值。
我们后续就可以用TCB来描述个由pthread.so库维护的结构体,毕竟它是上层封装,和LWP有区别了。这个时候我们就能理解,为什么对线程操作,都需要传入线程id,本质上对线程的管理都是对维护线程属性的结构体的操作。
(2)资源划分和维护
①新线程栈的动态创建和销毁
当主线程开始执行时,它就会保证自己有一个栈,即主线程栈,这个栈通常是在程序启动时由操作系统预先分配好的,它的大小一般由操作系统和编译环境决定。当程序运行完毕后,这个栈才会销毁。这个栈的位置就在进程地址空间的栈区。
除此之外,其它所有子线程的栈都是动态创建的。为什么叫动态创建?只有当调用create函数,创建子线程时,这个栈才会被创建。同理,只要这个线程被join(或分离线程执行完毕),那么它的栈就会被销毁。因此,子线程栈的动态性在于它可以在程序运行途中创建和销毁,而主线程栈是一进程序就创建,结束才销毁。
②struct pthread的存储
struct pthread(TCB)被pthread.so库维护,就意味着从物理内存的角度上看,一个系统的所有线程的属性都存在pthread.so库的内部。当pthread.so被映射到虚拟内存中后,就存放有所有线程的TCB,当我们对某个线程进行操作时,都是对这个库中某个线程属性的修改。同理,FILE也是如此,glibc这个库中维护了整个系统所有的FILE结构体。
struct pthread里面主要含有两个属性,线性局部存储和线性栈的属性,除此之外还一定包括LWP的属性task_struct,就像FILE里面一定有文件描述符fd。
③线性局部存储
对于代码全局区的变量来说,每个线程都可修改它们的值,并且修改之后其它线程都能同时感受到这个变化。但有的时候我们不希望这样,这就要用到编译性关键字__thread,用它修饰变量后(如__thread int a = 10),每个线程都会独立地得到该数据,不会互相影响,这就叫线程局部存储。
线性局部存储的数据是线程私有的数据,错误码error的私有性质就是用线程局部存储实现的,这样就能正确标识每个线程的错误了,而不会相互影响。
__thread只能修饰内置类型,结构体不行。局部存储的变量会单独存到TCB的一个属性里,因此对一些高频访问效率较高,不用到线性栈里面去找。
④线性栈和mmap
后面会提到的mmap区域就是指的共享区。
前面已经说过主线程栈在栈区,可以动态增长。新线程的栈在共享区,也就是mmap区域被开辟,并且它有一个最大容量值,即最多开辟8MB(随系统,用完就没了,不像主线程栈可以向下增长),这些栈的起始地址、容量信息都会被存入线程的属性结构体TCB中。我们还应知道,mmap还是一个系统调用,充当开辟空间的作用,存在mmap区域的新线程栈就是由mmap系统调用申请的。malloc的底层也是mmap。
线程的栈区原则上是独立的,每个线程各自用各自的,但实际上是共享的,因为没有权限限制,加上所有线程看到的都是同一个进程地址空间,所以只要拿到别的线程的栈空间地址,就能访问。堆区、数据段同理。
到此我们可以总结一下,线性栈和线性局部存储都由struct pthread(TCB)管理,都存在共享区,其中栈是由mmap系统调用单独在mmap(共享)区开辟空间。这些TCB最终都由pthread.so维护,事实上,pthread.so维护了整个系统的线程属性struct pthread。
(3)clone
这个系统调用的几个参数需要声明
fn是函数入口地址,arg是函数的参数包,轻量级进程就会根据这两个参数执行对应函数。stack是LWP占用的独立的栈的地址,LWP会在指定栈保存变量。flags是标志位,区分不同的操作。
我们从参数就能理解,调用pthread_create就需要先后调用mmap、clone。先是由mmap开辟空间,更新TCB,再由clone来执行。在系统层这是新建了一个LWP并执行代码,而在用户层看来,这就是创建了一个新的线程。
我们还可以了解一下clone,它也是vfork和fork的底层调用,clone是更底层的系统调用,我们用户无法直接调用。
(4)创建线程过程
当程序开始运行时,主线程栈在栈区创建并向下增长。当要创建新线程时,会创建struct pthread,会调用系统调用mmap在mmap(共享)区域动态创建栈且栈的大小不可增长,之后由pthread.so维护的线程的属性struct pthread会进行更新,之后通过clone系统调用创建好一个LWP,并将pthread.so维护的struct pthread的地址返回给用户作为id。之后用户的所有操作本质都是调用pthread.so的函数对struct pthread的属性进行修改。
相关文章:

Linux相关概念和易错知识点(28)(线程控制、Linux下线程的底层)
目录 1.线程控制 (1)pthread和thread库 (2)线程的创建、等待和分离 ①线程创建 ②线程等待 ③线程分离 ④线程替换(不可行) (3)线程的终止和取消 ①线程终止 ②线程取消 2…...
lighten() 函数被弃用:替代方案color.scale()或者color.adjust()
在 SCSS (Sass 的一个语法) 中,lighten() 函数用于调整颜色的亮度。然而,随着 Sass 语言的不断发展,一些旧函数被标记为弃用,以鼓励使用更现代、更灵活的 API。lighten() 函数就是其中之一。 1. 弃用通知 当您看到 lighten() is…...

【leetcode】双指针:有效三角形的个数 and 和为s的两个数
文章目录 1. 有效三角形的个数1.题目2.讲解算法原理3.代码 2.和为s的两个数1.题目2.思路3.代码 1. 有效三角形的个数 1.题目 示例1解析: 2.讲解算法原理 3.代码 class Solution { public:int triangleNumber(vector<int>& nums) {sort(nums.begin(), …...

IDEA通过Contince接入Deepseek
Deepseek 的出色表现,上期【Deepseek得两种访问方式与本地部署】 安装Continue插件 第一步、下载插件 在编辑栏【File】->设置【Settiings】或快捷键【CtrlAltS】,弹窗的左侧导航树,选择【plugins】,在marketplace 搜索【Continue】,点…...
grep如何排除多个目录?
在使用 grep 进行文本搜索时,有时候需要排除多个目录,避免在这些目录下进行搜索。下面介绍几种不同的实现方式。 目录 1.使用 -r 和 --exclude-dir 选项(GNU grep) 2.使用扩展正则表达式和 -P 选项(GNU grep&#x…...
Elasticsearch 数据建模:从原理到实战的降维打击指南
Elasticsearch 数据建模:从原理到实战的降维打击指南 🚀 第一章 数据建模的物理法则:倒排索引的奇妙世界 1.1 倒排索引:比字典更聪明的数据结构 当你在ES中存入"Hello World"时,背后发生了这些魔法&#…...
python defaultdict用法
摘要 使用 defaultdict 可以简化处理字典中缺失键的情况。以下是几个使用 defaultdict 的示例,展示了它在不同场景下的应用。 示例 1:分组文件 假设我们有一组文件名,想要根据文件扩展名将它们分组。我们可以使用 defaultdict 来实现这一点…...
Java 与设计模式(15):模板方法模式
一、定义 模板方法模式是一种行为设计模式,它定义了一个操作中的算法的骨架(也就是大致的步骤和流程),而将一些具体步骤的实现延迟到子类中。这样,子类可以不改变算法的结构即可重新定义算法的某些特定步骤。 二、Ja…...
ubuntu更新失败:apt-get install -f Transaction failed: 软件包系统已损坏
检查您是否使用了第三方源。如果是就禁用它们,它们常常导致问题。 然后在终端中运行以下命令:apt-get install -f Transaction failed: 软件包系统已损坏下列软件包未满足的依赖关系:sunloginclient: Depends: libappindicator3-1 但是 %%s 没…...
16-使用QtChart创建动态图表:入门指南
QtChart是Qt框架中的一个强大模块,用于创建各种类型的图表,如折线图、柱状图、饼图等。它提供了丰富的API和灵活的配置选项,使得开发者能够轻松地将数据可视化集成到应用程序中。本文将介绍如何使用QtChart创建一个简单的动态折线图ÿ…...
C++ | 虚函数
在 C 面向对象编程领域,多态性堪称核心概念,而虚函数则是实现运行时多态的关键所在。 一、虚函数的概念与作用 1.1 什么是虚函数 虚函数是 C 中用于实现动态多态的成员函数。在基类中使用virtual关键字声明虚函数后,派生类能够重写&#x…...
单元测试整理
在国外软件开发中,单元测试必不可少,但是国内并不太重视这一块,一个好的单元测试可以提前发现很多问题,也减去和测试battle的时间 Spring单元测试 JUnit4 RunWith 指明单元测试框架 e.g. RunWith(SpringJUnit4ClassRunner.cla…...
Delphi语言的软件工程
Delphi语言的软件工程 引言 在软件工程的历史长河中,Delphi语言作为一种快速应用程序开发(RAD)的工具,凭借其高效的开发环境和强大的编程能力,一直在软件开发领域占有一席之地。本文将探讨Delphi语言的历史背景、特性…...
XSS攻击(跨站脚本攻击)详解与实战
文章目录 一、什么是XSS?二、XSS分类与场景三、XSS攻击实战流程四、CTF中的XSS利用五、XSS防御方案六、绕过过滤的常见技巧七、实战练习资源 一、什么是XSS? XSS(Cross-Site Scripting) 是一种通过向网页注入恶意脚本(…...

【C++指南】类和对象(十):const成员函数
💓 博客主页:倔强的石头的CSDN主页 📝Gitee主页:倔强的石头的gitee主页 ⏩ 文章专栏:《C指南》 期待您的关注 目录 引言 一、const成员函数的定义与语法 1. 基本语法 2. 底层原理 二、const成员函数的作用与约束…...
数值分析与科学计算导引——误差与算法举例
文章目录 第一章 数值分析与科学计算导引1.1 数值分析的对象、作用与特点数值分析的对象数值分析的作用数值分析的特点 1.2 数值计算的误差误差分类误差与有效数字数值运算的误差估计 1.3 算法举例秦九韶算法求多项式值开根号迭代算法牛顿切线加权平均的松弛技术 第一章 数值分…...
ubuntu安装docker 无法拉取问题
sudo docker run hello-world [sudo] ubuntu 的密码: Unable to find image hello-world:latest locally docker: Error response from daemon: Get "https://registry-1.docker.io/v2/": context deadline exceeded (Client.Timeout exceeded while awai…...

【C++项目】Rpc通信框架设计
目录 Rpc远程调用的思想 项目框架设计 服务端模块划分 网络通信模块 Network 应用层通信协议模块 Protocol 消息分发处理模块 Dispatcher 远程调用路由功能模块 RpcRouter 编辑 发布订阅功能模块 Publish-Subscribe 服务注册/发现/上线/下线功能模块 Registry-Disc…...
八股取士--dockerk8s
一、Docker 基础 Docker 和虚拟机的区别是什么? 答案: 虚拟机(VM):虚拟化硬件,每个 VM 有独立操作系统,资源占用高,启动慢。Docker:容器化应用,共享宿主机内核…...
Autojs: 使用 SQLite
例子 let db new SQLiteUtil("/sdcard/A_My_DB/sqlite.db");db.fastCreateTable("user_table",{name: "",online: false,},["name"] // 设置 name 为唯一, 重复项 不会添加成功 );// 新增数据的 ID let row_id db.insert("use…...

IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...
postgresql|数据库|只读用户的创建和删除(备忘)
CREATE USER read_only WITH PASSWORD 密码 -- 连接到xxx数据库 \c xxx -- 授予对xxx数据库的只读权限 GRANT CONNECT ON DATABASE xxx TO read_only; GRANT USAGE ON SCHEMA public TO read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; GRANT EXECUTE O…...

【从零学习JVM|第三篇】类的生命周期(高频面试题)
前言: 在Java编程中,类的生命周期是指类从被加载到内存中开始,到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期,让读者对此有深刻印象。 目录 …...
LangChain 中的文档加载器(Loader)与文本切分器(Splitter)详解《二》
🧠 LangChain 中 TextSplitter 的使用详解:从基础到进阶(附代码) 一、前言 在处理大规模文本数据时,特别是在构建知识库或进行大模型训练与推理时,文本切分(Text Splitting) 是一个…...
【HarmonyOS 5】鸿蒙中Stage模型与FA模型详解
一、前言 在HarmonyOS 5的应用开发模型中,featureAbility是旧版FA模型(Feature Ability)的用法,Stage模型已采用全新的应用架构,推荐使用组件化的上下文获取方式,而非依赖featureAbility。 FA大概是API7之…...
ThreadLocal 源码
ThreadLocal 源码 此类提供线程局部变量。这些变量不同于它们的普通对应物,因为每个访问一个线程局部变量的线程(通过其 get 或 set 方法)都有自己独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,这些类希望将…...

leetcode73-矩阵置零
leetcode 73 思路 记录 0 元素的位置:遍历整个矩阵,找出所有值为 0 的元素,并将它们的坐标记录在数组zeroPosition中置零操作:遍历记录的所有 0 元素位置,将每个位置对应的行和列的所有元素置为 0 具体步骤 初始化…...

Linux-进程间的通信
1、IPC: Inter Process Communication(进程间通信): 由于每个进程在操作系统中有独立的地址空间,它们不能像线程那样直接访问彼此的内存,所以必须通过某种方式进行通信。 常见的 IPC 方式包括&#…...