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

线程概念,控制

一、线程概念

线程概念进程内部的一个执行流,轻量化

观点:进程是系统分配资源的基本单位,线程是CPU调度的基本单位

在理解线程之前,我们在谈一下虚拟地址空间

我们都知道进程是通过页表将虚拟地址转化为物理地址的,对于PCB,file我们已经了解了,所以,我们主要谈页表

虚拟地址和物理地址之间映射时,是通过字节映射的吗如果进程大小是4GB,那么一共就会有 4 * 1024 * 1024 * 1024个字节,如果是按照字节映射,那么页表的大小(虚拟地址和物理地址各占4字节,其它不考虑)就是 8 * 4 * 1024 * 1024 * 1024个字节,也就是32GB,一个进程的页表就这么大,这是不可能的。所以,肯定不是通过字节映射的

我们都知道磁盘和物理内存之间是通过4KB进行IO的,在逻辑上我们认为物理内存也是以4KB划分的物理内存上4KB划分的空间,我们把它叫做页框或者页帧如何理解页框呢

在物理内存中会有许多这样的4KB空间,有些空间被使用,有些未被使用…,那么OS要不要对这些空间进行管理呢?答案是要的,先描述在组织

在OS有一个 struct page就是用来描述物理内存4KB空间的,一个4KB空间对应一个 struct page,那么对空间已经描述了,那么该怎么组织呢?只需要用一个 struct page pages[]数组来管理就可以了,对物理内存的管理就转变为了对数组的增删查改

既然如此,在OS内部还需要保存物理地址这样的概念吗

不需要了,这个数组中每个内存块的大小是固定的(4KB),那么物理块的起始地址 = 数组下标 * 4KB,申请一个物理内存块,本质只要申请到 struct page,知道 struct page的下标,那么物理内存块的所有地址就都知道了

那么OS如何得知所有物理内存块的地址呢OS只需要得到 page数组的起始地址即可

结论文件,进程和物理内存之间的关系就转化为了 file,task_struct 和 page之间的关系了

在32位系统下,OS采用的是二级页表,从虚拟地址转换到物理地址,默认是没有直接转化到字节的。虚拟地址一共32个 bit,从左往右依次划分10个 bit,10个 bit,12个 bit,根据CR3寄存器里存储的页表起始地址,使用前10个 bit用来索引一级页表,一级页表中存在1024个页表项(存储的是下一级页表的起始地址),中间的10个 bit用来索引二级页表,二级页表存储的是物理页的起始地址,这样就可以找到物理页框的起始地址了,最后12个 bit用来做页内偏移

查页表只需要帮我们找到要访问的是哪一个页框就可以了

真正的物理地址 = 页框起始地址 + 页内偏移

页表的大小 = 4 * 1024,就是4KB的大小,每一个页表都是4KB大小,那么二级页表一共(1024 + 1)* 4KB的大小,对比于32GB,那可真是小太多了

细节1CR3寄存器保存的是当前进程页表的基地址,物理地址

细节2虚拟地址高20位相同,一定是连续存放在一个页框的,因为索引的时候访问的都是同一个页表的同一个位置

细节3如果知道任意一个虚拟地址,如何得到所处的页框

addr & 1111 1111 1111 1111 1111 0000 0000 0000

那如何得到 page结构体呢page 存储在一个结构体数组里,只需要得到数组下标就可以了,数组下标 = 页框号 / 4KB

细节4进程首次加载磁盘块的时候,OS做什么

内存管理,申请内存就是申请 page,得到 page的数组下标,进而得到页框的物理地址,填充页表

细节5如果访问的是 int呢?一个结构体呢?一个类变量呢所有变量只有一个地址,开辟空间时最小字节的地址

页表转换的时候,只能拿到第一个字节的地址,所以语言中存在一个类型的概念。起始地址 + 偏移量的方式就可以访问了

细节6如何理解写时拷贝

OS内,申请和管理内存是以4KB为单位的,写时拷贝也是以4KB为单位的,申请一个新的页表,更改映射关系

细节7我们用 new,malloc申请,怎么申请的时候1,4,n字节随意申请的呢

new,malloc底层一定要调用系统调用(brk,mmap),只有OS才能访问硬件,调用系统调用是有成本的,所以C,C++自己在语言层,会有自己的内存管理机制,类似STL中的空间配置器

现在,再来理解什么是线程。

//thread线程标示符,类似于进程pid,输出型参数
//attr,线程的属性,通常设置为nullptr
//start_routine回调函数,函数指针类型
//arg作为回调函数的参数
//成功返回0,失败返回错误码
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, 
void*(*start_routine)(void*), void* arg); //创建线程,执行指定的回调函数


在这里插入图片描述

在这里插入图片描述

大家有没有发现问题呢?一个单进程代码,竟然同时让两个死循环跑起来了

一个可执行程序,一个进程有一套页表,那么,进程页表的本质是什么?是进程看到资源的"窗口",通过虚拟地址与物理地址的映射,看到内存当中的代码和数据

一个进程,两个死循环同时跑起来了,是让不同的线程执行不同的函数,本质是让不同的线程,通过拥有不同区域的虚拟地址,拥有不同的资源,通过函数编译的方式,进行了进程内部的"资源划分"

Linux中多线程的实现:

一个进程中可以有一个执行流,那么可以有两个,多个执行流吗?当然是可以的那么这些执行流(线程)也是需要被OS管理,OS需要对这些线程分配新的页表,文件,调度算法等资源吗?答案是不需要的只需要给线程分配PCB就可以了,线程是进程内部的一个执行流,执行的是进程内部的一部分代码资源没有必要浪费这么多的资源为线程分配新的页表等

Linux中,一个线程在进程内部运行,是如何运行的呢线程在进程的虚拟地址空间中运行,线程和进程共享同一个虚拟地址,页表等资源(体现了线程是进程内部的一个执行流)

如何体现线程的轻量化呢让不同的线程访问虚拟地址空间中的一部分资源

那么,要如何才能做到,让不同的线程看到自己的代码资源呢?以代码区为例:

让不同的线程未来执行不同的入口函数即可(函数编译的方式,进行进程内部资源的划分)

在Linux中,线程的实现是用进程模拟的,复用了进程代码和结构

那么,今天我们要如何理解进程和线程呢?

以前我们说 进程 = PCB + 自己的代码和数据可是今天进程里有许多的PCB,这要如何理解呢

以前我们讲的进程是内部只有一个执行流的进程,也叫做单线程的进程,而今天,我们需要对进程重新定义。

进程 = OS分配的所有 task_struct + 自己的代码和数据 + 页表、文件等资源

所以,我们说进程是承担分配系统资源的基本实体

在CPU的角度,是不区分线程和进程的,它只拿着 task_struct 进行资源的调度,所以,执行流我们把它叫做轻量级进程

线程(task_struct)自然而然就是CPU调度的基本单位了

验证:

ps -aL //查看所有的轻量级进程

在这里插入图片描述
在这里插入图片描述

Linux中不存在线程概念,只存在轻量级进程的概念,所以,Linux系统给用户提供系统调用,只能提供轻量级进程的系统调用

在这里插入图片描述

所有创建进程或者线程的系统调用底层都对 clone 进行了封装

但是这个系统调用使用起来非常麻烦,所以创建线程时需要使用pthread库,这个库对clone这个函数做了封装

CPU在获取物理地址时其实并不是直接通过MMU查找页表得到物理地址的,而是通过TLB(快表,其实就是缓存),如果TLB有虚拟地址到物理地址的映射就给CPU,否则就去查找页表,在页表中找到之后,把物理地址给CPU,同时把这条虚拟地址和物理地址的映射给TLB,进行缓存

线程的优点

1.创建一个新线程比创建一个新进程的代价小得多(进程需要创建PCB,虚拟地址空间,文件资源,页表等,线程只需要创建PCB,共享进程的其它资源)

2.与进程之间的切换相比,线程之间的切换需要OS做的工作要少很多

. CPU内有CR3寄存器,保存的是页表的基地址,进程间切换需要更新CR3寄存器的内容,线程间切换不需要,因为,同一个进程里所有线程拥有的是同一个页表

. TLB就是缓存虚拟地址和物理地址的映射关系,线程间切换TLB不需要更新(线程共享进程的虚拟地址空间),进程切换TLB需要更新

. CPU内有一个 cache 硬件,这个硬件就是用来缓存代码和数据的,在CPU访问内存中的代码和数据时,并不是不断的进行虚拟地址到物理地址之间的映射访问的,而是通过 cache 硬件访问的,cache 硬件会预先加载一部分代码和数据,线程切换时,cache 是不需要更新的,进程切换需要重新加载新的代码和数据

在这里插入图片描述
在这里插入图片描述

可以看到,cache 大小还是挺大的(这与系统有关,也有MB的)

3.线程占用的资源比进程少很多(线程拥有进程的完整资源,但线程不需要重复再分配共享资源,如虚拟地址空间,页表等,线程只需要维护少量资源,比如局部变量,函数调用,寄存器状态)

线程的缺点

1.性能损失(过多的线程使用同一个处理器,增加了线程调度,而可用资源不变)

2.健壮性降低(多个线程共享了不该共享的变量,缺乏保护)

3.缺乏访问控制(调用某些OS函数对整个进程造成影响)

线程独有的数据

线程ID

寄存器(线程上下文数据)

进程间多线程共享

同一地址空间(代码段,数据段...)

文件描述符表

每种信号的处理方式

当前工作目录

用户id,组id

写一段程序验证一下。

在这里插入图片描述
在这里插入图片描述

可以看到,线程之间是共享全局变量,函数和堆空间的。当然了,这只是一部分,毕竟线程是共享进程的虚拟地址空间的

二、线程控制

在这里插入图片描述
在这里插入图片描述

主线程运行3秒后结束,新线程10秒后才终止,但是主线程一旦退出,所有的线程都退出了,表示进程终止了

这是因为,进程创建时OS需要分配PCB等资源,那么当进程退出时,所有的资源也应该都要进行回收,所以,所有的线程都退出了

一般情况下,主线程应该最后退出,线程也需要等待,类似进程的 wait。要对新线程进行等待,否则,也会造成类似僵尸进程的问题

//成功返回0,失败返回错误码
//thread表明等待哪一个线程
//retval获取新线程退出时的退出信息
//阻塞等待,main thread最后退出,自动解决新线程的内存泄漏问题(僵尸问题)
int pthread_join(pthread_t thread, void** retval);

在这里插入图片描述
在这里插入图片描述

可以看到,在多线程等待时,一旦只要有一个线程崩溃,所有的线程都崩溃了,而进程之间具有独立性,即便是父子进程,子进程崩溃也不会影响父进程。所以说多线程的缺点是健壮性低

线程终止

. return

. exit

在这里插入图片描述
在这里插入图片描述

exit是用来终止进程的,变相导致所有的线程退出

. pthread_exit

void pthread_exit(void* retval);//线程退出

在这里插入图片描述
在这里插入图片描述

可以看到,使用系统调用退出线程,只会让调用该函数的线程退出,不会影响到其它线程

. pthread_cancel

//成功返回0,失败返回非0的错误码
//thread取消目标线程的线程标识符
//线程退出,退出信息设置为-1(PTHREAD_CANCELED,是一个宏值)
int pthread_cancel(pthread_t thread); //取消线程

在这里插入图片描述
在这里插入图片描述

通常用于主线程取消其它线程

那么,线程自己可不可以取消自己呢?

先认识一个系统调用。

//返回的是调用线程的id
pthread_t pthread_self(void);

在这里插入图片描述
在这里插入图片描述

可以看到,主线程创建新线程的 tid 与新线程获取自己的线程标识符是一样的

在这里插入图片描述
在这里插入图片描述

可以看到,线程自己取消自己也是可以的。但是,这里为什么会将这条语句打印两次呢

这是因为,pthread_cancel函数发送取消请求,对应的线程收到取消请求之后会在合适的点终止自己,不是立即终止

最佳实践取消线程的方法在主线程中使用 pthread_cancel,本来就是主线程取消其它线程的

线程的传参和返回值问题

前面我们介绍了 pthread_join 函数它的第二个参数就是将线程退出时的退出信息带出来,现在,我们就要聊聊这个参数了,它是怎么通过这个参数把线程的退出信息带出来的,毕竟我们只是使用了两个系统调用而已。

还记得C语言中的 fopen函数吗,它的返回类型是 FILE*类型的文件指针。那么这个FILE是什么呢?它有在哪里

这个前面我们是说过的,FILE是一个结构体,它在C标准库里,fopen函数返回文件指针的时候,就必然创建了一个FILE对象那么这个对象在哪里呢它应该就在fopen函数内部申请的,然后通过 return 返回

那么,在多线程这里,线程的概念是谁提供的pthread库提供的

那么,将来我们可以在一个进程中创建很多线程,在多个进程中呢?就会有更多的线程,所以,线程需不需要被管理呢?那些线程在被调度,那些线程退出了?答案是需要的

那么,就应该对线程进行先描述在组织像进程一样有一个结构体 struct tcb,那么,这个结构体在哪里呢?不要忘了,前面说了,线程的概念是pthread库提供的,所以,这个结构体应该在 pthread库里面。这个结构体中就会有线程的各种属性

将来线程退出时,return 将数据写入到结构体中,主线程在等待时,将等待线程的标示符 tid传入进去,就可以找到指定的线程了(结构体),然后通过第二个参数将结构体中的退出信息拷贝出来,不就拿到指定线程的退出信息了吗

分离线程

默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join操作,否则,会造成类似僵尸进程的问题(资源泄露)

如果不关心线程的返回值,我们可以告诉系统当线程退出时,自动释放线程资源。这个时候就不需要进行 pthread_join了。

//成功返回0,失败返回错误码
//thread分离线程的线程标示符
int pthread_detach(pthread_t thread);

分离线程可以自己分离自己,也可以是其它线程分离目标线程

在这里插入图片描述
在这里插入图片描述

线程分离之后再去等待线程,就会出错

在这里插入图片描述
在这里插入图片描述

这个时候可能有人要问了,主线程把新线程分离之后,如果是主线程先退出呢。那进程都终止了,新线程不是也会终止吗?这个问题不用担心,因为真正的软件都是死循环的,新线程执行完自己的代码就会退出,主线程是最后退出的

接下来再聊一下,pthread_cancel函数取消目标线程之后得到的退出信息

在这里插入图片描述
在这里插入图片描述

三、线程ID及虚拟地址空间布局

在这里插入图片描述

我们说过,线程是由 pthread 库提供的,所以线程是依赖于 pthread 库的,将来 pthread 库也要被加载到内存里。那么,一个进程中可以有许多线程,也可以加载许多进程啊这些进程中都会包含许多子线程,那么这些进程也是需要将 pthread 库映射到自己的虚拟地址空间中的,调用mmap 系统调用实现的

在这里插入图片描述

前面我们说过,线程有几部分资源是独占的,线程id、一组寄存器(线程的硬件上下文数据)、栈。描述线程的结构体是由 pthread库维护的,线程栈并不是在虚拟地址空间中的栈区上的,而是在共享区上,由 pthread 库在共享区上申请的一块固定的内存空间,主线程的栈是在虚拟地址空间上的

那么,什么是线程局部存储

前面我们说过,全局变量也是多线程之间共享的,那么如果我们要使线程之间独自私有呢?

在这里插入图片描述
在这里插入图片描述

像这样的就是线程局部存储

今天的内容分享就到这里了,觉得不错的给个一键三连吧。


文章转载自:

http://vbH9NwCR.gpmrj.cn
http://Xkt3MZvY.gpmrj.cn
http://RJbNGZb9.gpmrj.cn
http://Of0BaarW.gpmrj.cn
http://5Sgd0ZZ8.gpmrj.cn
http://xKKDEKj0.gpmrj.cn
http://mI4KxH3z.gpmrj.cn
http://xdBqdoLu.gpmrj.cn
http://kKP3CvSp.gpmrj.cn
http://uyjWkcD7.gpmrj.cn
http://tVGjkaqC.gpmrj.cn
http://jZBCcw7I.gpmrj.cn
http://X2c6vhAU.gpmrj.cn
http://rmhqG5l6.gpmrj.cn
http://wRuzgfEB.gpmrj.cn
http://B8Kro3Bc.gpmrj.cn
http://yMbvKbbM.gpmrj.cn
http://2SRfnBU3.gpmrj.cn
http://1lHZQU9k.gpmrj.cn
http://L34wZTpw.gpmrj.cn
http://hjiZi5Yi.gpmrj.cn
http://06yDWLRh.gpmrj.cn
http://r2lBDDDQ.gpmrj.cn
http://R3C6oN3W.gpmrj.cn
http://A589kPjx.gpmrj.cn
http://a2a4Fuae.gpmrj.cn
http://lSsYB5EB.gpmrj.cn
http://yixeQzFP.gpmrj.cn
http://nvZZ1y3e.gpmrj.cn
http://jANACZhv.gpmrj.cn
http://www.dtcms.com/a/385431.html

相关文章:

  • 扫描仪常见样式:平板与馈纸的特性与适用场景
  • Python进程和线程——多线程
  • 2025年AIOCR审核革命!七大智能费控报销系统终结手工录入
  • 从循环到矩阵运算:矢量化加速机器学习的秘诀
  • R 语言入门实战|第七章 程序:从“老虎机”项目学透流程控制与代码优化
  • clickhouse 中SUM(CASE WHEN ...) 返回什么类型?
  • NR帧结构
  • 【联合查询】
  • 常见IC封装详解:从DIP到BGA的演进与应用
  • DockerComposeUI+cpolar:容器管理的远程可视化方案
  • tcp的三次握手与四次挥手简介
  • 2025算法八股——深度学习——MHA MQA GQA
  • 常见岩性分类与油气勘探意义笔记
  • 贪心算法应用:内存分配(First Fit)问题详解
  • RTK基站模块技术要点与作用解析
  • Istio与系统软中断:深度解析与问题排查全指南
  • 常用命令整理
  • PrestaShop 后台 Session 权限错误与产品链接 404 错误的解决指南
  • springboot“期待相遇”图书借阅系统的设计与实现(代码+数据库+LW)
  • SQLAlchemy -> Base.metadata.create_all(engine )详解
  • JVM 三色标记算法详解!
  • BUMP图改进凹凸贴图映射
  • 嵌入式硬件——I.MX6U-Mini 蜂鸣器(BEEP)模块
  • LeetCode 2799.统计完全子数组的数目
  • 蚂蚁T19 Hydro 158T矿机评测:强劲算力与高效冷却技术
  • Kafka架构:构建高吞吐量分布式消息系统的艺术——核心原理与实战编码解析
  • CCAFusion:用于红外与可见光图像融合的跨模态坐标注意力网络
  • 用 Python 玩转 Protocol Buffers(基于 edition=2023)
  • 配置文件和动态绑定数据库(上)
  • 整体设计 之 绪 思维导图引擎 之 引 认知系统 之 序 认知元架构 之 认知科学的系统级基础设施 框架 之1