当前位置: 首页 > news >正文

Linux -- 线程概念与控制

本文重点:

1. 深刻理解线程

2. 深刻理解虚拟地址空间

3. 了解线程概念,理解线程与进程区别与联系。

4. 学会线程控制,线程创建,线程终⽌,线程等待。

5. 了解线程分离与线程安全概念。

6. 掌握线程与进程地址空间布局

7. 理解LWP和原⽣线程库封装关系


1. 深刻理解线程

  1.1什么是线程:线程就是进程中的一个执行分支,是一个单独的执行流。一个进程中至少有一个线程,线程在进程的内部运行,本质是在进程的地址空间运行。在Linux系统中,CPU看到的PCB比传统的进程更轻量化称为轻量化进程。线程在Linxu中用到的数据结构正是轻量型进程,并没有为它新创建一个类似TCB的数据结构,而是复用了以前的代码。进程中的地址空间是每个线程共享的,这样线程就可以看到进程的大部分资源,将进程资源进行合理分配给每个执行流,就形成了线程执行流。另外:进程是承担系统分配资源的实体,线程是系统中调度的基本单位,我们之前学到的进程都是只有一个执行分支的进程,即只有一个PCB。

2. 深刻理解虚拟地址空间

  2.1在真正理解线程之前我们必须要明白内核是如何进行资源划分的,这就要从分页式存储管理谈起。

  2.1.1虚拟地址和页表的由来:如果没有虚拟地址以及页表,那么我们数据和代码在内存中就应该是连续存放的,因为每⼀个程序的代码、数据⻓度都是不⼀样的,按照这样的映射⽅式,物理内存将会被分割成各种离散的、⼤⼩不同的块。经过⼀段运⾏时间之后,有些程序会退出,那么它们占据的物理内存空间可以被回收,导致这些物理内存都是以很多碎⽚的形式存在。我们希望操作系统提供给⽤⼾的空间必须是连续的,但是物理内存最好不要连续。此时虚拟内存和分⻚便出现了,如下图所⽰:

  在32位的系统中,物理内存往往会被分割为一个个4KB大小的页框(一个储存区域),每个页框又包含一个页(数据块),每个页的大小等于页框的大小。操作系统会将虚拟内存下的逻辑地址空间分为若干页,将物理内存分为若干页框,通过页表将连续的虚拟内存映射到若干个不连续的物理内存中。这样就解决了使用连续的物理内存造成的碎片化问题。

  2.2.1提到了页框又不得不提到物理内存的管理了,假设一个可以使用的物理内存大小是4GB,按照我们上面所说一个页框的大小是4KB,那么4GB的空间就应该有一百多万的页框。这么多的页框OS也必须将他们管理起来,所以在内核中使用了struct page结构表⽰系统中的每个物理⻚,出于节省内存的考虑, struct page 中使⽤了⼤量的联合体union。

/* include/linux/mm_types.h */
struct page {
/* 原⼦标志,有些情况下会异步更新 */
unsigned long flags;
union {
struct {
/* 换出⻚列表,例如由zone->lru_lock保护的active_list */
struct list_head lru;
/* 如果最低为为0,则指向inode
* address_space,或为NULL
* 如果⻚映射为匿名内存,最低为置位
* ⽽且该指针指向anon_vma对象
*/
struct address_space* mapping;
/* 在映射内的偏移量 */
pgoff_t index;
/*
* 由映射私有,不透明数据
* 如果设置了PagePrivate,通常⽤于buffer_heads
* 如果设置了PageSwapCache,则⽤于swp_entry_t
* 如果设置了PG_buddy,则⽤于表⽰伙伴系统中的阶
*/
unsigned long private;
};//内核中的一小部分

  其中有几个比较重要的参数:(1)flags,⽤来存放⻚的状态。这些状态包括⻚是不是脏的,是不是被锁定在内存中等。flag的每⼀位单独表⽰⼀种状态,所以它⾄少可以同时表⽰出32种不同的状态。这些标志定义在<linux/page-flags.h>中。其中⼀些⽐特位⾮常重要,如PG_locked⽤于指定⻚是否锁定,PG_uptodate⽤于表⽰⻚的数据已经从块设备读取并且没有出现错误。(2)_mapcount,表示此时页表中有多少项指向该页,也就是该页被引用了多少次。当计数值为-1时表示当前内核并没有引用这一页,可以在新的分配中使用它。

  系统中有一百多万个page,一个page占40个字节,那么储存这么多的结构体也就消耗了40MB,因此在系统中管理这么多的物理页的代价并不算太大。

  page在物理内存实际上是用一个伙伴系统类似数组的形式进行管理的,所以只要找到了page就有了下标,就能够知道物理地址(下标*4KB),同样的有了物理地址就能找到这个page下标对这个页进行管理。

  2.2.3页表:在32位的系统中一共有2^32个地址,每个地址是4个字节,那么页表用来储存这些地址的内存大小就应该是2^32*4=16GB,而我们的物理内存也就4GB,这显然是不合理的,所以需要采用其他的方式来对页表进行管理。在Linux中采用了多级页表来进行管理,这里用一般的二级页表进行讲解。在OS中首先有一张大页表,其中包含着1024个页目录表项,这其中的每个页目录表项又包含着1024个页表项,每个页表项存的是每个page的起始地址,使用4个字节存储,所以一个多级页表所消耗的内存是2^10*2^10*4=4MB。那么到这里大家肯定就很疑惑为什么这样就能将2^32个地址储存起来了,一个虚拟地址是32位的,我们先使用前十位进行索引在哪一个页目录,然后再用次十位进行索引页表项,接着还有最后12位作为偏移去页表项中存着的page的起始地址进行查找,4KB刚好等于2^12,所以次12位就能够查找这一个page范围内的所有地址了。Linux正是用这种极为智慧的方法来极大降低了页表的大小。一个进程是用不完4GB的内存的,所以实际上它的页表一定时不完整的。

  下⾯以⼀个逻辑地址为例。将逻辑地址( 0000000000,0000000001,11111111111 )转换为物
理地址的过程: 1. 在32位处理器中,采⽤4KB的⻚⼤⼩,则虚拟地址中低12位为⻚偏移,剩下⾼20位给⻚表,分成两级,每个级别占10个bit(10+10)。2. CR3 寄存器 读取⻚⽬录起始地址,再根据⼀级⻚号查⻚⽬录表,找到下⼀级⻚表在物理内存中存放位置。3. 根据⼆级⻚号查表,找到最终想要访问的内存块号。4. 结合⻚内偏移量得到物理地址。通过以上步骤虚拟地址就可以转换成物理地址了,这个转换的操作是集成在CPU内部的硬件MMU完成的。
  到这⾥其实还有个问题,MMU要先进⾏两次⻚表查询确定物理地址,在确认了权限等问题后,MMU再将这个物理地址发送到总线,内存收到之后开始读取对应地址的数据并返回。那么当⻚表变为N级时,就变成了N次检索+1次读写。可⻅,⻚表级数越多查询的步骤越多,对于CPU来说等待时间越⻓,效率越低。
  总结:单极页表对连续内存要求高,于是引入了多级页表,多级页表虽然降低了连续储存要求且减小了空间,但是同时会降低了查询的效率。
  在Linux中添加了一个TLB缓存来解决上面效率慢的问题,当CPU给MMU传新地址的时候,MMU会先去TLB查找,如果有就直接拿到物理地址发到总线给内存,但是TLB的容量较小,会发生Cache Miss,这时MMU会继续查页表,在页表中找到物理地址以后除了把地址发到总线传给内存,还把这条映射关系传给TLB,让它记录一下刷新缓存。

  2.3缺页异常:设想,CPU 给 MMU 的虚拟地址,在 TLB 和⻚表都没有找到对应的物理⻚,该怎么办呢?其实这就是缺⻚异常 Page Fault ,它是⼀个由硬件中断触发的可以由软件逻辑纠正的错误。假如⽬标内存⻚在物理内存中没有对应的物理⻚或者存在但⽆对应权限,CPU 就⽆法获取数据,这种情况下CPU就会报告⼀个缺⻚错误。由于 CPU 没有数据就⽆法进⾏计算,CPU罢⼯了⽤⼾进程也就出现了缺⻚中断,进程会从⽤⼾态切换到内核态,并将缺⻚中断交给内核的 Page Fault Handler 处理。

  缺⻚中断会交给 PageFaultHandler 处理,其根据缺⻚中断的不同类型会进⾏不同的处理:

  Hard Page Fault 也被称为 Major Page Fault ,翻译为硬缺⻚错误/主要缺⻚错误,这时物理内存中没有对应的物理⻚,需要CPU打开磁盘设备读取到物理内存中,再让MMU建⽴虚拟地址和物理地址的映射。

  Soft Page Fault 也被称为 Minor Page Fault ,翻译为软缺⻚错误/次要缺⻚错误,这时物理内存中是存在对应物理⻚的,只不过可能是其他进程调⼊的,发出缺⻚异常的进程不知道⽽已,此时MMU只需要建⽴映射即可,⽆需从磁盘读取写⼊内存,⼀般出现在多进程共享内存区域。

  Invalid Page Fault 翻译为⽆效缺⻚错误,⽐如进程访问的内存地址越界访问,⼜⽐如对空指针解引⽤内核就会报 segment fault 错误中断进程直接挂掉。

  如何区分是缺⻚了,还是真的越界了? (1)⻚号合法性检查:操作系统在处理中断或异常时,⾸先检查触发事件的虚拟地址的⻚号是否合法。如果⻚号合法但⻚⾯不在内存中,则为缺⻚中断;如果⻚号⾮法,则为越界访问。(2) 内存映射检查:操作系统还可以检查触发事件的虚拟地址是否在当前进程的内存映射范围内。如果地址在映射范围内但⻚⾯不在内存中,则为缺⻚中断;如果地址不在映射范围内,则为越界访问。线程资源划分的真相:只要将虚拟地址空间进⾏划分,进程资源就天然被划分好了。

  什么是 “页面不在内存中”?虚拟内存的核心是:程序的虚拟页面不需要全部加载到物理内存中,只有正在使用或近期可能使用的页面才会被加载,其他页面暂时存放在硬盘(如 Windows 的 “页面文件”、Linux 的 “交换分区”)。因此,“页面不在内存中” 指的是:页号是合法的(即该虚拟页面属于程序的地址空间,比如程序申请的堆内存、代码段对应的页面);但该虚拟页面当前没有被加载到物理内存,而是存在硬盘的交换区中。

3. 理解线程与进程区别与联系。 

  3.1线程的优点:

(1)创建一个新线程的代价比创建一个新进程的小得多。

(2)与进程切换相比,线程之前的切换需要操作系统做的工作少的多:

  线程切换以后虚拟内存空间仍然是相同的,但是进程切换时不同的。这两种上下文切换的处理都是通过操作系统内核来完成的,这种切换过程伴随的最显著性能的损耗是将寄存器中的内容切出。

  另一个损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,⼀旦去切换上下⽂,处理器中所有已经缓存的内存地址⼀瞬间都作废了。还有⼀个显著的区别是当你改变虚拟内存空间的时候,处理的⻚表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在⼀段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。

(3)线程占用的资源比进程少

(4)能充分利用多处理器的可并行数量

(5)在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务

(6)计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现

(7)I/O密集型应⽤,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

4. 学会线程控制,线程创建,线程终⽌,线程等待。

  4.1 POSIX线程库

  (1)与线程有关的函数构成了⼀个完整的系列,绝⼤多数函数的名字都是以“pthread_”开头的

  (2)要使⽤这些函数库,要通过引⼊头⽂ <pthread.h>

  (3)链接这些线程函数库时要使⽤编译器命令的“-lpthread”选项

  4.2 创建一个线程

功能:创建⼀个新的线程
原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
参数:
thread:返回线程ID
attr:设置线程的属性,attrNULL表⽰使⽤默认属性
start_routine:是个函数地址,线程启动后要执⾏的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码

  错误检查:pthreads函数出错时不会设置全局变量errno(⽽⼤部分其他POSIX函数会这样做)。⽽是将错误代码通过返回值返回,pthreads同样也提供了线程内的errno变量,以⽀持其它使⽤errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要⽐读取线程内的errno变量的开销更⼩。

  这里写了一个新线程与主线程同步进行的示例:

  这里用pthread_self来获取了主线程和新线程各自的id,以及在命令行中查看确实是有两个线程在同步进行,所以证明确实是创建了新线程。

  打印出来的 tid 是通过 pthread 库中有函数 pthread_self 得到的,它返回⼀个 pthread_t 类型的
变量,指代的是调⽤ pthread_self 函数的线程的 “ID”。怎么理解这个“ID”呢?这个“ID”是 pthread 库给每个线程定义的进程内唯⼀标识,是 pthread 库维持的,转成十六进制我们会发现其实就是该进程在地址空间中自己的内存空间的起始地址。由于每个进程有⾃⼰独⽴的内存空间,故此“ID”的作⽤域是进程级⽽⾮系统级(内核不认识)。其实 pthread 库也是通过内核提供的系统调⽤(例如clone)来创建线程的,⽽内核会为每个线程创建,系统全局唯⼀的“ID”来唯⼀标识这个线程。
4.3 线程终止 
线程的终止通常有三种方式:(1)在线程函数内部return,这种方式在主线程中相当于调用exit,如果在子线程中调用exit会将整个进程退出。(2)线程可以调用pthread_exit终止自己。(3)一个线程可以调用pthread_cancel终止同一进程的另一进程。
pthread_exit函数:
功能:线程终⽌
原型:
void pthread_exit(void *value_ptr);
参数:
value_ptr:value_ptr不要指向⼀个局部变量。
返回值:
⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调⽤者(⾃⾝)
pthread_cancel函数:
功能:取消⼀个执⾏中的线程
原型:
int pthread_cancel(pthread_t thread);
参数:
thread:线程ID
返回值:成功返回0;失败返回错误码

  4.4线程等待

  线程与进程一样需要等待,因为已经退出的线程的空间还没有被释放,仍然在进程的地址空间内,创建的新线程不会复用刚才推出线程的地址空间。

pthread_join函数
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID
value_ptr:它指向⼀个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用pthread_join函数以后调用线程会阻塞等待,直到等待的线程终止。线程以不同的方式终止。通过pthread_join得到的状态是不同的:
1. 如果thread线程通过return返回,value_ ptr所指向的单元⾥存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调⽤pthread_ cancel异常终掉,value_ ptr所指向的单元⾥存放的是常
数PTHREAD_ CANCELED。
3. 如果thread线程是⾃⼰调⽤pthread_exit终⽌的,value_ptr所指向的单元存放的是pthread_exit的参数。
4. 如果对thread线程的终⽌状态不感兴趣,可以传NULL给value_ ptr参数。
示例代码:
这里我们可以发现三个不同的线程使用不同的方式结束线程,但是他们的id都是一样的,这是因为三个线程都是逐步的,即复用了上一个线程的地址空间,并且在等待的时候主线程是在等待线程结束才会向下继续执行的。

5. 了解线程分离与线程安全概念。

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进⾏pthread_join操作,否则
⽆法释放资源,从⽽造成系统泄漏。
如果不关⼼线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,⾃
动释放线程资源。
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对⽬标线程进⾏分离,也可以是线程⾃⼰分离:
pthread_detach(pthread_self());

6. 掌握线程与进程地址空间布局

  pthread_t 到底是什么类型呢?取决于实现。对于Linux⽬前实现的NPTL实现⽽⾔,pthread_t类
型的线程ID,本质就是⼀个进程地址空间上的⼀个地址。pthread_ create函数第⼀个参数指向⼀个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。

7. 理解LWP和原⽣线程库封装关系

  LWP 是什么呢?LWP 得到的是真正的线程ID。之前使⽤ pthread_self 得到的这个数实际上是⼀个地址,在虚拟地址空间上的⼀个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线 程ID,线程栈,寄存器等属性。 在 ps -aL 得到的线程ID,有⼀个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,⽽其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。⽽pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。

线程库的封装:

http://www.dtcms.com/a/333223.html

相关文章:

  • Spring Boot 静态函数无法自动注入 Bean?深入解析与解决方案
  • 死锁总结及解决方案
  • MetaFox官方版:轻松转换视频,畅享MKV格式的便捷与高效
  • AutoSar AP平台功能组并行运行原理
  • 数据结构——顺序表单链表oj详解
  • 2025戴尔科技峰会:破局者的力量与智慧
  • Android 协程实用模板
  • Nature Commun.:6GHz Ku波段无斜视波束成形!光子准TTD技术实现无限分辨率
  • 【Unity3D实例-功能-拔枪】角色拔枪(一)动态创建武器
  • 数据库SQL
  • FT61F145芯片解密-程序发展
  • 用 1 张 4090 复现 GPT-3.5?——单卡 24 GB 的「渐进式重计算」训练实践
  • 【秋招笔试】2025.08.15饿了么秋招机考-第三题
  • 【BLE系列-第四篇】从零剖析L2CAP:信道、Credit流控、指令详解
  • RK3588消费级8K VR一体机 是否有坑?
  • 【HarmonyOS】鸿蒙应用迁移实战指南
  • AI+脱口秀,笑点能靠算法创造吗
  • rem 适配方案
  • [论文阅读] 软件工程工具 | EVOSCAT可视化工具如何重塑软件演化研究
  • Autosar之CanNm模块
  • redis升级版本迁移数据
  • 一个集成多源威胁情报的聚合平台,提供实时威胁情报查询和播报服务、主动拦截威胁IP,集成AI等多项常用安全类工具
  • 非中文语音视频自动生成中文字幕的完整实现方案
  • 另类pdb恢复方式-2
  • RabbitMQ核心架构与应用
  • C++类与对象核心知识点全解析(下)
  • 《Python列表和元组:从入门到花式操作指南》
  • 系统介绍pca主成分分析算法
  • Kubernetes 集群镜像资源管理
  • 区块链:用数学重构信任的数字文明基石