【操作系统内核】线程

【操作系统内核】线程

为什么需要线程

比如我要做一个视频播放器,就需要实现三个功能:

① 从磁盘读取视频数据

② 对读取到的视频数据进行解码

③ 对解码的数据进行播放

  1. 如果串行执行(通过一个进程来执行):
20230805151957

那么播放一会就需要等待数据从磁盘加载(读磁盘很慢,会使得这个进程阻塞,CPU空置),然后通过CPU解码,就会一卡一卡的

  1. 如果三个进程来执行,分别负责IO的读写、CPU解码以及播放

进程1读磁盘内容,然后传递给进程2解码,再传递给进程3播放,这样就产生了两个问题:

  • 创建了三个进程,实现一个简单的功能却耗费过多的系统资源
  • 进程间的内存空间不一致,数据时独立,进程之间传递数据,需要操作系统协调(频繁陷入内核)完成,效率低

线程解决进程开销大的问题

① 线程直接共享进程的所有资源 (比如 mm_struct),所以线程就变轻了,创建线程比创建进程要快到 10 ~ 100 倍

② 线程之间共享相同的地址空间 (mm_struct),这样利于线程之间数据高效的传输

③ 可以在一个进程中创建多个线程,实现程序的并发执行

什么是线程:进程中的一条执行流(函数调用链),用于执行不同路径的代码指令,每个进程一开始都有一个主线程

20230805155929

因此,进程可视为由两部分组成:资源平台(地址空间、磁盘、网络资源等)、线程

线程可访问的三类数据

线程共享mm_struct,所以其执行的代码指令是存放在进程地址空间的代码段中

  1. 线程栈

前文说了线程就是一条函数调用链,所以每个线程需要有自己私有的线程栈,存放在当前进程的堆中

而主线程(如main函数)的栈则使用进程的栈

20230805160700

线程栈从高地址向低地址生长

  1. 全局变量(读/写数据段)

  2. 线程私有变量

线程创建代码实例 pthread_create():

20230805160859

线程私有数据设置:

  • 创建一个私有数据key:pthread_key_create(“key”)
  • 设置私有数据:线程 1:pthread_setspecific(“key”, 22)
  • 获取私有数据:线程 1:pthread_getspecific(“key”)
20230805161312

pthread_create详细过程

由于一个进程会有多个线程栈,可以用两个链表来管理这些线程栈:

  • stack_used: 还未退出的线程的线程栈
  • stack_cache: 退出的线程的线程栈,缓存在堆中,下次其他线程启动时直接可以用

pthread创建线程是由内核态和用户态合作实现的,也就是先在用户态创建一个线程(pthread实例),然后在切换到内核态再创建一个线程(task_struct实例):

用户态(创建一个用户态的线程):

  • 调用pthread_create()
    • 根据设置栈的大小,从stack_cache中找到相应大小的线程栈;如果没有,申请堆空间创建线程栈
    • 创建pthread实例(包含了线程私有数据、栈大小、入口函数等),将之放在线程栈栈底位置
  • 调用create_thread()
    • clone()系统调用:将子线程要执行的函数代码起始指令位置、参数写入寄存器(很重要) => 到此为止都是主线程在执行

内核态(创建一个内核态的线程管理用户态的线程):

  • 将主线程的寄存器信息保存到主线程内核栈中

  • 调用do_fork()(创建进程也是用的do_fork(),所以进程线程的创建都差不太多)

    • 创建task_struct以及对应的内核栈

    • 创建进程时,需要复制复制父进程的实例,但线程时资源共享的,不需要复制主线程的实例,直接将线程task_struct的实例指针指向进程的实例指针即可

      20230805163821
  • 维护线程的亲缘关系,主要是维护线程和所属进程的关系

    • 进程的pid等于其tgid,其中tgid表示所属进程的id,据此操作系统可区分一个task是进程还是线程
    • 另外group_leader表示task所属的进程组
  • 将task_stuct加入链表队列

在内核的角度,线程和进程的区别并不大,只是进程需要多一份资源管理

Tip:

  • pthread创建用户线程需要内存创建用户态线程栈,内核创建内核线程需要内存分配(slab分配器)创建内核态线程栈,所以线程的数量不是无限的,会耗尽内存
  • 不管是创建用户态的线程,还是内核态的线程,开销都很小,消耗性能的动作主要是系统调用,会发生CPU上下文切换

所以,为减少CPU的上下文切换,可以建立线程池,当线程执行完后,把线程还给线程池(在用户态阻塞),而非操作系统,后续再重用这个线程,同时,设置最大线程数量,防止内存不足

主线程的CPU上下文恢复

  1. 线程创建完成后,将从主线程的内核栈获取CPU上下文切换到用户态,对比进程创建完成后切换到内核态,此时:

用户态的栈就是父进程的栈,栈顶指针也指向父进程的栈,指令指针也是指向父进程的代码

那么切回到用户态将会进入主线程

  1. 但clone()这个系统调用不一样,它在进入内核态之前,就把要执行的函数代码起始地址(也就是入口函数的地址)写入寄存器,进入内核后,存入内核栈的自然是子线程的下一条指令,此时:

用户态的线程栈就是创建线程A的栈,栈顶指针也指向线程A的栈,指令指针也是指向线程A的代码

然后执行start_tread(),执行线程函数

  1. 那么问题又来了,子线程倒是能顺利执行,那主线程怎么办,主线程的CPU上下文都没了:

但其实在内核拿到子线程CPU上下文,准备返回用户态的那一刻,主线程和子线程进行了一次线程切换参考链接,主线程的CPU上下文信息写入了其内核栈,等下次调度主线程时,就可以顺利运行了

用户级线程和内核级线程

PCB与TCB:

操作系统每创建一个进程,都会在内核态创建一个进程管理器PCB: Process Control Block,存入进程表

操作系统每创建一个线程,都会创建一个线程管理器TCB: Thread Control Block(如果是创建用户级线程,则TCB必须存放在用户态),存入线程表

用户级线程

用户级线程:由一些应用程序中的线程库来实现,应用程序可以调用线程库的 API 来完成线程的创建、线程的结束等操作

20230805173518

用户级线程优点:

  • 快,线程的创建、销毁、切换都非常快,不需要陷入内核态
  • 可以自定义调度算法,比较灵活

缺点:

  • 一个线程不让出CPU,其他的线程永远执行不到了,因此只有线程主动让出cpu,线程库才有切换线程的权力(如果有内核管理的话,会进行时钟中断)
  • 如果一个线程被阻塞,那么这个线程所有的线程都会被阻塞。
    • 比如我一个进程中的一个子线程A需要调用系统资源,则需要陷入内核找到对应的PCB去访问资源,这个过程中,子线程A被阻塞,其他线程也拿不到CPU的执行权,就整个进程都阻塞了
  • 操作系统看不到线程,只能以进程的视角调用,很可能分配的执行时间太少

内核级线程

内核级线程:在内核空间实现的线程,由操作系统管理的线程;内核级线程管理的所有工作都是由操作系统内核完成,比如内核线程的创建、结束、是否占用 CPU 等都是由操作系统内核来管理。

20230805180142

在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息 (PCB 和 TCB),一个进程的 PCB 会管理这个进程中所有线程的 TCB,当一个线程阻塞,那么内核可以选择另一个线程继续运行。=> 比如Linux

在Linux中,pthread_create会创建一个用户级线程 + 一个内核级线程,pthread_create创建一个TCB,内核会创建一个内核级线程(task_struct)来管理这个用户态线程

Tip: 这里的内核级线程也叫轻量级进程LWP

内核级线程的优点:

  • 内核级线程的创建、终止和切换都是由内核来完成的,所以应用程序如果想用内核级线程的话,需要通过系统调用来完成内核级线程的创建、终止和切换,这里会涉及到用户态和内核态的转换,因此相对于前面用户级线程,系统开销较大

缺点:

  • 在一个进程中,如果某个内核级线程因为发起系统调用而被阻塞,并不会影响其他内核线程的运行。因为内核级线程是被操作系统管理,受操作系统调度的

  • 因为内核级线程是调度单位,所以操作系统将整个时间片是分配给线程的,多线程的进程获得更多的 CPU 时间

用户级线程和内核级线程的关系

不管怎样,线程的实现都需要用户态和内核态的相互配合,因此产生了如下几种关系:

  1. 用户级线程 to 内核级线程: n to 1

线程的TCB存放在用户态,通过一个task_struct访问系统资源,也就是用户级线程,这种线程模式线程切换快,开销小

20230805181703
  1. 用户级线程 to 内核级线程: 1 to 1

线程的TCB存放在内核态,也就是内核态线程,如上文讲的pthread, 这种线程模式并发能力强

20230805182642
  1. 用户级线程 to 内核级线程: m to n

比如Go中的协程,需要根据自定义的调度器进行切换

20230805182717

内核线程

不管是创建进程(fork)还是创建线程(clone),都需要在内核调用do_fork()

202308061454307

而内核线程也可以通过kernel_thread()调用dofork()来创建

202308061457745

与内核级线程不同,内核线程不能访问用户态内存空间

  • active_mm:用于指向进程所处的虚拟地址空间 (用户态或者内核态)
  • mm: 用户态虚拟地址空间
  • init_mm: 内核态虚拟地址空间,全局只有一个

当进程处于内核态时,指向内核态的地址空间active_mm=mm;当进程处于用户态时,指向用户态的地址空间;active_mm=init_mm

202308061504355

而内核线程的mm=null,因此不能访用户态虚拟地址空间

Tip:1号进程如何从内核进程转变为普通进程?

  1. 先加载可执行文件,设置mm
202308061513248
  1. 设置寄存器,切换到用户态(为数不多从内核态切到用户态的)
202308061516009

线程的状态

在工作中,线程池是肯定会遇到的,会经常遇到线程的状态的变化,一般线程的状态为:创建、就绪、运行、阻塞、结束

202308061525614

还是一个状态很重要:挂起

阻塞挂起:当一个线程处于阻塞时,而其他运行中的线程需要的内核又很多,系统会把这个阻塞线程的内存交换到磁盘,即使等待的事件到达了,也只能转变为就绪挂起状态
阻塞解挂:当磁盘中的数据加载到内存后,线程的状态就从阻塞挂起变成了阻塞

同理,就绪状态的线程也可能会挂起

而处于运行中的线程,如果也因为内存不够,就会转变为就绪挂起状态

202308061539976

Linux线程的状态

  1. task_running: Linux线程没有就绪状态,或者说就绪状态和运行状态的值都是task_running,但Linux会把一个专门用来指向当前运行任务的指针 current 指向它,以表示它是一个正在运行的线程。
202308061548326
  1. TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE:阻塞状态(可中断和不可中断)

正常来说,一个线程需要进行IO操作,此时将会阻塞,等待IO操作完成后,再继续执行

202308061552838

但现在,在阻塞的时候,其他线程发了一个kill- 9的命令,如果是可中断的阻塞,需要响应这个信号,杀死自己;而如果是不可中断,则不会响应这个信号

202308061557745

不可中断的阻塞是个很危险的事情,一旦 I/O 操作因为特殊原因不能完成,这个时候,谁也叫不醒这个进程了;所以一般只有内核线程才会设置这个状态,比如执行磁盘IO(DMA搬运数据被打断可能会产生严重问题)时

总结下线程的执行效率比进程高

  1. 线程创建直接重用进程的资源即可,不需要额外维护,线程释放也不需要考虑资源释放的问题
  2. 线程间数据共享,不需要切内核就可以访问共享数据
  3. 线程切换要快,进程的切换需要切换进程对应的页表,需要 flush TLB,而刷新TLB后页表项都不会命中LTB,需要去内存查找页表,而线程共享页表

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/132788.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【MySQL】rank()、row_number()、dense_rank()用法详解

建表测试 测试表数据:test1 CREATE DATABASE /*!32312 IF NOT EXISTS*/db_test /*!40100 DEFAULT CHARACTER SET utf8 */; USE db_test; /*Table structure for table test1 */ DROP TABLE IF EXISTS test1; CREATE TABLE test1 ( id int(10) NOT NULL, score i…

数据结构从未如此简单——图(一)

文章目录 前言图的初印象教科书力扣工作中的实际应用我们的学习方法 前言 个人感觉数据结构学习最大的难点就是抽象。这些概念和算法都是从许多源问题中抽离、精炼、总结出来的。我们学习的看似是最精华的部分,但是忽略了推导过程,很容易变成死记硬背&a…

C语言求解有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?

完整代码: /*有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月 又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? 程序分析: 兔子的规律为数列1,1,2,3,5,8…

MySQL中的json使用注意

MySQL中json是一种重要的数据类型 好的点在于其不必事先定义列得名称啥的 不过不要将明显的关系型数据作为json来存储,例如用户余额、姓名、身份证等,这些是用户必须包含的数据 json适合存储的是给每个用户(或者物品)打的标签&…

超简单的Linux FTP服务搭建教程

目录 前言1、检查vsftp是否已安装2、安装vsftpd3、启动ftp服务4、测试ftp服务5、上传文件配置总结 前言 本文记录了在Kylin Linux Desktop V10(SP1)系统上搭建FTP服务的过程。FTP是File Transfer Protocol的缩写,译为文件传输协议,是用于在网络上进行文…

docker复制镜像文件

一、复制镜像 #1. 查找本机已有的镜像docker images |grep xxxx#2. 将镜像复制出来指向到xxxx.tar的文件中 docker save 343cca04e31d > xxxx.tareg: 二、加载镜像 直接将拷贝好的镜像包直接加载即可 docker load < myimage.tar

NO.304 二维区域和检索 - 矩阵不可变

题目 给定一个二维矩阵 matrix&#xff0c;以下类型的多个请求&#xff1a; 计算其子矩形范围内元素的总和&#xff0c;该子矩阵的 左上角 为 (row1, col1) &#xff0c;右下角 为 (row2, col2) 。 实现 NumMatrix 类&#xff1a; NumMatrix(int[][] matrix) 给定整数矩阵 …

组成原理备考学习 day2 (2.1)

组成原理备考学习 day2 第二章 数据的表示和运算2.1 数制和编码2.1.1 进位计数法进制转换BCD码 2.1.4 字符2.1.5 校验原理2.1.6 海明校验码2.1.7 循环冗余校验码 第二章 数据的表示和运算 2.1 数制和编码 2.1.1 进位计数法 进制转换 16进制的字母为H BCD码 2.1.4 字符 2.…

基于CST的电磁感应透明设计与机制研究

前言 电磁感应透明&#xff08;EIT&#xff09;最早在量子力学中提出&#xff0c;但是量子系统实验条件十分苛刻且费用较高&#xff0c;超材料的出现对电磁感应透明的研究提供了一种新的方法。利用超材料单元结构设计灵活&#xff0c;通过排列不同结构可以实现操控电磁波而且能…

Arduino到底适不适合做产品

文章目录 一、Arduino性能很低&#xff0c;不如树莓派等开发板&#xff0c;所以不要用Arduino做开发二、Arduino程序效率很低&#xff0c;所以不要用Arduino做开发三、Arduino只能开发玩具&#xff0c;不能做产品四、Arduino开发板成本太高&#xff0c;不适合做产品总结个人见解…

【图像分类】【深度学习】【Pytorch版本】 GoogLeNet(InceptionV2)模型算法详解

【图像分类】【深度学习】【Pytorch版本】 GoogLeNet(InceptionV2)模型算法详解 文章目录 【图像分类】【深度学习】【Pytorch版本】 GoogLeNet(InceptionV2)模型算法详解前言GoogLeNet(InceptionV2)讲解Batch Normalization公式InceptionV2结构InceptionV2特殊结构GoogLeNet(I…

[量化投资-学习笔记011]Python+TDengine从零开始搭建量化分析平台-MACD金死叉策略回测

在上一章节 MACD金死叉中结束了如何根据 MACD 金死叉计算交易信号。 目录 脚本说明文档&#xff08;DevChat 生成&#xff09;MACD 分析脚本安装依赖库参数配置查询与解析数据计算 MACD 指标判断金叉和死叉计算收益绘制图形运行脚本 本次将根据交易信号&#xff0c;模拟交易。更…

《数字图像处理-OpenCV/Python》连载(41)图像的旋转

《数字图像处理-OpenCV/Python》连载&#xff08;41&#xff09;图像的旋转 本书京东优惠购书链接&#xff1a;https://item.jd.com/14098452.html 本书CSDN独家连载专栏&#xff1a;https://blog.csdn.net/youcans/category_12418787.html 第 6 章 图像的几何变换 几何变换分…

数据分析实战 | 贝叶斯分类算法——病例自动诊断分析

目录 一、数据及分析对象 二、目的及分析任务 三、方法及工具 四、数据读入 五、数据理解 六、数据准备 七、模型训练 八、模型评价 九、模型调参 十、模型预测 一、数据及分析对象 CSV文件——“bc_data.csv” 数据集链接&#xff1a;https://download.csdn.net/d…

RocketMQ(一):基本概念和环境搭建

Spring源码系列文章 RocketMQ(一)&#xff1a;基本概念和环境搭建 目录 一、RocketMQ简介二、各个MQ产品的比较三、RocketMQ重要概念1、基本概念2、消息从发送到被消费的的流程3、生产和消费理解 四、RocketMQ安装1、下载RocketMQ2、解压并配置环境变量3、修改nameServer的运行…

微软和Red Hat合体:帮助企业更方便部署容器

早在2015年&#xff0c;微软就已经和Red Hat达成合作共同为企业市场开发基于云端的解决方案。时隔两年双方在企业市场的多个方面开展更紧密的合作&#xff0c;今天两家公司再次宣布帮助企业更方便地部署容器。 双方所开展的合作包括在微软Azure上部署Red Hat OpenShift&#xf…

学习c#的第四天

目录 C# 变量 C# 中的变量定义与初始化 接受来自用户的值 C# 中的 Lvalues 和 Rvalues 不同类型变量进行运算 静态变量 局部变量 C# 常量 整数常量 浮点常量 字符常量 字符串常量 定义常量 扩展知识 Convert.ToDouble 与 Double.Parse 的区别 静态常量和动态常…

Vue中的常用指令v-html / v-show / v-if / v-else / v-on / v-bind / v-for / v-model

前言 持续学习总结输出中&#xff0c;Vue中的常用指令v-html / v-show / v-if / v-else / v-on / v-bind / v-for / v-model 概念&#xff1a;指令&#xff08;Directives&#xff09;是Vue提供的带有 v- 前缀 的特殊标签属性。可以提高操作 DOM 的效率。 vue 中的指令按照不…

Hadoop入门——数据分析基本步骤

文章目录 1.概述2.分析步骤2.1第一步 明确分析目的和思路2.2第二步 数据收集2.3第三步 数据处理2.4第四步 数据分析2.5第五步 数据展现2.6第六步 报告撰写 3.总结 1.概述 2.分析步骤 2.1第一步 明确分析目的和思路 2.2第二步 数据收集 2.3第三步 数据处理 2.4第四步 数据分析 …

Java Web——HTTP协议

目录 1. HTTP协议概述 1.1. HTTP数据传输格式 1.2. HTTP协议特点 2. HTTP 1.0和HTTP 1.1 3. HTTP请求协议 3.1. GET方式请求协议 3.2. POST方式请求协议 3.3. GET请求和POST请求的区别 4. HTTP相应协议 4.1. 响应状态码 如果两个国家进行会晤需要遵守一定的礼节。所以…
最新文章