linux线程 | 一篇文章带你理解线程的概念

news2024/10/9 22:05:09

        前言:本篇讲述linux里面线程的相关概念。 线程在我们的教材中的定义通常是这样的——线程是进程的一个执行分支。 线程的执行粒度, 要比进程要细。 我们在读完这句话后其实并不能很好的理解什么是线程。 所以, 本节内容博主将会带友友们理解什么是线程!线程和进程的关系等等。现在, 开始我们的学习吧!

        ps:本节适合已经学习了进程的友友们进行观看哦。

目录

linux中的线程该如何理解

linux实现线程的方案

重新定义进程和线程

tcb

模拟线程

tcb与模拟线程的区别

如何分配线程

线程和进程的切换问题


linux中的线程该如何理解

         首先我们知道我们的进程看待自己所能看到的所有资源都是通过地址空间来看的。 所以,地址空间是进程的资源窗口。所以我们的进程如果想做任何事情, 诸如加载动态库, 申请内存, 查看变量等等操作就必须使用地址空间加页表的方案, 从物理内存当中找到我们的代码和数据或者堆空间 ——地址空间是进程的资源窗口。 如果我们今天cpu调度进程时, 就相当于task_struct在cpu所对应的运行队列中去排队。 

        另外, 在我们的子进程当中, 我们的子进程是拷贝一份父进程的task_struct和父进程的地址空间。 然后重新映射到物理空间的不同位置。 如同下图:

        但是呢, 如果我们这么设计: 不再拷贝进程的PCB以及地址空间了。 只拷贝PCB, 然后让拷贝的PCB分走一部分进程地址空间的代码区, 堆区, 等等各种区。 然后呢, 创建多个PCB, 这些PCB都和原本的PCB共享一个地址空间。 (如何划分这个地址空间成为若干份, 由后续的设计者规定)。 那么我们就会发现我们新创建的进程以及曾经的进程, 他们可以对地址空间进行一定程度上的分享, 此时我们就可以认为新创建出来的这些新进程, 他们执行的粒度要比我们原本的进程要细一些。 并且, 又因为这些新进程执行的代码是一整个一整个代码区的一部分, 所以就称为它是一个分支, 我们为了区分这种进程, 就叫做线程。 

linux实现线程的方案

        在linux中, 线程在进程的地址空间运行(为什么?任何执行流要执行, 必须要有资源!而地址空间是进程的资源窗口。又因为线程是进程内部的一个执行流, 所以线程在进程的地址空间运行!)

        在linux中, 线程的执行力度要比进程更细? 这是因为线程执行进程代码的一部分。

不同的操作系统对于线程的概念一定都是一样的, 但是不同的操作系统对于线程的管理或者线程的整体的实现方案不一样的。 linux的实现方案是上面的样子。 所以, 站在cpu的角度, cpu知不知道我们现在正在访问task_struct是线程还是进程呢? 或者说cpu需不需要知道呢?——答案是不需要关心cpu只有调度执行流的概念, 所以cpu只要访问代码就访问代码, 访问数据就访问数据。 cpu只要拿到代码和数据, 并不关心到底是进程还是线程。 

重新定义进程和线程

  •         线程:我们认为,线程是操作系统调度的基本单位!!
  •         进程:进程是承担分配系统资源的基本实体!

        (我们之前的理解是进程 = 内核数据结构task_struct+代码和数据 ------- v1)现在我们把整个的所有的线程, 以及地址空间, 页表和页表映射到的一点点物理内存叫做进程。 就如同下图:

        在操作系统中, 我们分配资源的方式是以进程为单位进行分配的。 就比如线程, 那么操作系统创建task_struct, 然后分配一部分地址空间——执行流是资源吗? 执行流就是task_struct, 就是资源。 

        所以,进程和线程的关系就是:进程里面就是包含线程的。 因为进程是操作系统分配资源的基本实体, 而我们的线程是进程内部的执行流资源。 

        那么, 我们重新理解进程, 如何理解以前的进程呢?

        操作系统以进程为单位为我们分配资源。 只不过我们当前的进程内部, 只有一个执行流!!——也就是说, 我们之前讲的才是特殊情况。 现在的多个线程是正常情况。 

        如果我们的一个进程内部真的有多个进程。 所以, 进程与线程的比率一定是1 : n的, 并且至少为1 : 1。 所以, 当进程执行的时候, 当前进程的状态是什么, 这个线程当前执行到什么位置了当前需要访问哪些资源, 期间要访问哪些资源。 这个线程是属于哪个进程的, 这个线程需要被切换吗, 什么时候被切换等等这些问题。那么我们知道, 线程一定比进程更多。 线程不是一创建就直接退出了。 也不是一创建就完成了。 是创建的时候就开始了, 操作系统要调用这个线程, 运行这个线程, 切换这个线程。 但是, 我们知道, 线程的个数是非常多的。 那么这么多的线程 , 这么多的操作需要执行。 所以操作系统要进行调度, 就一定需要管理起来。 而如何管理呢? 就是先描述再组织!

tcb

        对于大部分操作系统来说, 就是描述struct tcb。 就比如windows。 对于线程来说, 线程是属于一个进程的。 而一个进程的线程有很多, 有需要组织起来, 那么我们操作系统就需要描述一个数据结构组织线程, 然后将这个数据结构和一个进程组织起来, 还要和调度队列联系起来。 如果我们的线程出现问题, 那么又会影响我们的进程, 影响我们的调度队列等等。 这就太复杂了。 但是呢, 又不得不这么干, 所以windows就这么干了, 也就是struct tcb; 

模拟线程

        对于计算机世界来说, 我们要管理线程, 那么就势必要先描述在组织。 但是linux程序员们并不想windows程序员们一样重新描述和组织线程。 没有人规定必须用新的描述方式和组织方式管理线程。 linux的设计者们考虑到既然线程也有上下文, 也有各种状态。 进程task_struct也有, 所以就没有必要再单独为进程重新描述,重新组织了。 所以linux的设计者们就直接服用进程数据结构和管理算法了。 ——struct task_struct——模拟线程。 

        那么这样做之后, 如何区分线程, 进程呢?——如果我们的数据结构内部有一份资源, 那么就是进程。 如果里面有多个PCB, 那么这个整体就是线程。 而且未来我们不区分这个PCB是不是进程, 是不是线程, 我们统一叫做执行流。 而什么叫做进程呢?页表、 地址空间、 所有的线程、 一点点物理地址这些合起来才是进程(分配资源的基本实体), PCB就是执行流, 只不过以前进程只有一个PCB, 也就是只有一个执行流, 而现在线程有多个PCB, 多个执行流。

上面的tcb, 和模拟线程是两钟描述线程的方案。 但是对于模拟线程这个方案, 因为进程和线程是类似的。 所以线程如果再重新描述一次就势必会造成程序的复杂, 当出现问题的时候就不好处理, 但是如果使用模拟线程的方法, 程序就会更加简洁。 出现的问题一定会更少, 维护成本降低, 所以他的健壮性一定要比所有的操作系统要强, 而且强的不是一丁半点。 

tcb与模拟线程的区别

        无论是使用方案1还是方案2,, 都是具体的实现方案。 那么说linux中没有真正意义上的线程, 不是真的说linux没有线程。 对于tcb来说, tcb是将线程的概念体现在了代码上面。 而我们的linux并没有将线程的概念体现在代码上面, 而是体现在了人们的大脑当中!我们的windows通过tcb遵守了线程的概念。 我们的linux也通过模拟线程遵守了线程的概念!!并且, 我们是使用进程的内核数据结构模拟的线程。 

        操作系统, 什么叫做操作系统, 我们在学校上的课, 实际上是规定一款操作系统应该是什么样子。 或者说一款操作系统在设计上应该符合什么概念。 它是一款操作系统设计的指导手册, 但是具体我们在实现的时候, 不同的操作系统有不同的方案。

        题外话:我们的cpu在执行的时候, 当它看到一个pcb的时候, 执行一部分代码时, cpu执行的代码, 是进程的代码, 还是线程的代码呢? 其实cpu无法区分到底是进程还是线程。 但是站在上帝的视角我们知道, cpu执行的执行流, 一定是小于等于进程的。 在大部分OS中, 执行流小于进程就是线程, 等于就是进程(线程的粒度更细)。 在linux中的执行流被称为“轻量级执行流”。

        在我们国家, 承担分配社会资源的基本实体是什么呢?——家庭。 自古到今, 我们的社会都是以家庭为单位来向社会进行资源申请的。 然后呢, 我们就会发现, 在我们的家庭里:我们的爷爷奶奶每天就是打太极拳, 逛公园, 或者看别的老人下棋啥的, 目的就是把自己的身体养好。 我们的爸爸妈妈呢, 每天就是工作, 赚钱养家。 小孩在家就是学习。 那么我们会看到, 在这个家庭里面, 每一个人天然的就会存在一种小人物。 但是, 不管每一个人有着怎么样的小任务, 他们一定会有一个共同的任务——把自己家的日子过好。 只不过每一个人为了把日子过好, 领取的任务是不同的。 所以我们把家庭整体叫做进程, 把里面的爸爸妈妈, 爷爷奶奶, 我们自己叫做一个线程。 每一个线程和每一个进程的关系就是:线程是在进程的内部进行, 而且每一个线程能够做什么, 和我们进程所拥有的资源是有关的!! 而如果这个家庭里只有一个人, 就是我们进程的情况!!

如何分配线程

        如何分配线程, 这个就需要用到我们页表的知识。 我们要先谈页表结构, 页表的工作原理。 再来谈如何分配线程, 看下面一张图:

        当我们的程序加载到物理内存的时候, 页表有映射,那么从物理内存当中读取到cpu里面的地址是什么呢? 读到的是虚拟地址cpu读到虚拟地址后, cpu再从地址空间找到对应的位置, 通过页表映射就能够执行这个进程了。 

        那么, 我们以32位为例, 谈一谈虚拟地址是如何转化到物理地址的。 如果在32位条件下虚拟地址有多少位的呢? 答案也是32位。  那么, 首先我们对应的虚拟地址有32位, 并且这里需要知道的是这32位虚拟地址不是一个整体。 这32位虚拟地址我们把它转化为了10 + 10 + 12。 

        而且, 页表不是一整块的。 如果页表是一整块的, 我们知道, 页表中的条目最多2 ^ 32个, 如果是整块的, 那么我们一个条目假设有10字节那么我们的内存就爆了, 所以, 页表一定不是一整块的。 页表是分为三个板块, 其中有一级页表、二级页表、还有一个页框级别页表。 就如同下图:

        什么意思呢, 意思就是说, 我们未来从cpu内读到的某个虚拟地址, 这个虚拟地址一共有32位。 假如这个虚拟地址是1000 0000 1000 0010 0100 1000 0010 0010. 那么它其实存放的规则是按照10 + 10 + 12来存放的。 如下:

       

        并且把他们分成第一个部分, 第二个部分, 第三个部分。所以, 将来会用我们第一个部分查找我们的第一季页表。 用第二部分查找我们的第二级页表。 第三部分, 查找我们的最后一个页表。 而查找的过程就是先将对应的各个部分转化为十进程。 再由十进制找到三个页表中的下标索引。 而我们的页表的前20位, 对应的就是先查第一级页表, 再查第二级页表。 查完后, 就已经能够查找到我们的物理内存对应的页框了!!

        然后, 我们的第三个表, 指向的就是页框内具体的偏移量。 也就是说, 我们通过二级页表查找到了具体的哪一个页框, 然后得到了这个页框的地址, 假如是0x0012ff40。 那么再通过最后12位来获取偏移量。 最后0x0012ff40 + 虚拟地址最后12位。 就能得到最终的结果。 

        那么我们思考一下, 这个时候的页框, 最大是多少呢? 

我们知道我们的页表当中也有一些权限字段, 是可读还是可写等等。 一共会有1024个二级页表。 假设我们一个二级页表项是4个字节。 所以一个二级页表4 * 1024byte, 即4kb。 所以将来一个页表是可以放到一个页框里面的。 那么我们一个进程, 最多会有多少4kb呢? 因为页目录最多1024个, 所以我们最终要乘以1024. 即最多会有4 * 1024kb = 4MB。 那么4MB大不大呢? 这个空间挺大的, 但是这个空间和上面的虚拟地址空间比起来已经很小很小了。 而且, 我们一个进程, 会把整个地址空间用完吗? 不会的。另外, 我们的内核地址空间, 是不需要给每个进程都维护一份的。 并且, 我们的每个进程不一定把每个地址空间全部弄完。 所以, 大部分进程根本就不能把第一个一级页表全部用完。 所以我们想说的是, 二级页表不一定全部存在。 (在大部分情况下都是不全的。但是, 其实创建一个进程依旧是一个很“重”的工作。)

        那么看这个问题, 这里有int a = 10; 按道理来说, int类型有4字节, 那么就有4个地址。 但是为什么&a只拿到了一个地址呢? 这是因为我们有类型的存在。 我们的int类型, 只需要知道首地址, 然后之后的4个字节就按照偏移量向后找就行。 c/c++里面的自定义的类,归根结底就是我们一大堆内置类型的集合。 所以, 对于自定义的累, 取地址我们就会拿到第一个地址。 所以我们的起始地址 + 类型 = 起始地址 + 偏移量(这也是x86cpu的特点)

        所以, 如何理解资源分配? 线程资源分配的本质, 不就是分配地址的空间范围吗? 我们的线程分配资源, 本质就是把我们的地址空间划分一部分。 那么这个划分难不难呢? 怎么划分呢? 就比如我们有10个函数, 这个函数有没有地址? 代码有没有地址? 那么我们的某个线程使用了这个函数, 使用了某个代码, 那么他就天然的具有地址了。 

线程和进程的切换问题

        我们说线程比进程更加轻量化,为什么呢?

  •         a:创建和释放更加轻量化(生死问题)
  •         b: 切换更加轻量化(运行问题)

        总结就是整个生命周期线程都比进程更加轻量化。

        我们知道的是线程他自己肯定要有自己的上下文。 但是线程在切换的时候, 他对应的页表需要切换吗? 不需要。 地址空间需要切换吗? 不需要。 所以, 线程在切换的时候, 只是在局部切换, 它的页表和地址空间都不需要切换。 那么, 为什么说它的切换效率更高呢? 

        这个涉及到了cpu的知识。 就是当线程在运行的时候, 其实本质上就是进程在运行, 线程是进程的执行分支。在cpu当中, 除了有寄存器, cpu还会有一个硬件级别的cache。 cpu认为和物理内存交互太慢了, 所以就在自己里面集成了一块cache空间。 这块空间相对于寄存器很大, 相对于内存不大。 这部分cache被称为缓存的热数据。 

        那么, 线程切换, 在同一个进程内的线程切换 cache内的数据不需要或者说很少需要重新缓存。 但是进程切换, 那么cache内的数据要丢失并且重新缓存, 数据从冷变热, 需要花费时间。所以, 线程的切换要更加轻量, 而进程要更加重。

——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!  

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vscode软件中可以安装的一些其他插件

一. 简介 前面了解了 在 做 C开发时 vscode软件需要安装的一些常用插件。文章如下: vscode软件在 C发中常用插件-CSDN博客 本文继续了解一些其他好用的插件。 二. vscode软件中可以安装的一些其他插件 1. 字体插件 FIRA CODE FIRA CODE 致力于提升代码的连贯…

Chromium 中chrome.history扩展接口c++实现

一、前端 chrome.history定义 使用 chrome.history API 与浏览器的已访问网页的记录进行交互。您可以在浏览器的历史记录中添加、移除和查询网址。如需使用您自己的版本替换历史记录页面,请参阅覆盖网页。 更多参考:chrome.history | API | Chrome…

LSTM 长短期记忆网络:解锁时间序列数据的深层秘密

在这个数据驱动的时代,理解和预测时间序列数据成为了许多领域的关键。从股票价格预测到天气模式分析,从自然语言处理到健康监测,时间序列数据无处不在,并且蕴含着丰富的信息。然而,传统的神经网络在处理这类数据时往往…

Openstack 安装教程

1.首先更新系统 sudo apt update sudo apt upgrade -y2.安装必要软件包 sudo apt install -y software-properties-common3.添加openstack官方仓库 sudo add-apt-repository cloud-archive:train sudo apt update4.安装openstack核心组件 sudo apt install -y python3-opens…

技术分享 —— JMeter接口与性能测试实战!

前言 在软件开发和运维过程中,接口性能测试是一项至关重要的工作。JMeter作为一款开源的Java应用,被广泛用于进行各种性能测试,包括接口性能测试。本文将详细介绍如何使用JMeter进行接口性能测试的过程和步骤。 JMeter是Apache组织开发的基…

Redis-02 持久化

redis持久化即将数据从内存写入磁盘,Redis提供了两种持久化的方式:RDB和AOF。 1.RDB RDB持久化:Redis可以将内存中的数据定期快照保存到磁盘上的一个二进制文件中。RDB持久化是一种比较紧凑的文件格式,适用于备份和灾难恢复。通过…

陈零九全新单曲《也曾想走进你的心底》 揭露爱而不得的情感遗憾

图片提供:种子音乐 “创作男神”陈零九于10月9日推出充满深情的全新创作单曲《也曾想走进你的心底》,这首歌再次延续他招牌的“九式情歌”风格,展现其创作魅力。歌曲以一段“爱而不得”的感情故事为主线,深入探讨人们在爱情中的复…

java家政预约上门系统源码,家政服务平台源码,基于SpringBoot框架,数据库使用MySQL,界面渲染采用Thymeleaf技术开发

自主知识产权的家政预约上门系统源码,java版本,支持二次开发,适合商用上项目。 在这个快节奏的现代生活中,越来越多的家庭开始寻求高效、便捷的家政服务解决方案。传统的家政服务模式已经很难满足人们日益增长的个性化与即时性需求…

GAMES202作业3

EvalDiffuse 对于一个diffuse的着色点,它的BRDF为: /** Evaluate diffuse bsdf value.** wi, wo are all in world space.* uv is in screen space, [0, 1] x [0, 1].**/ vec3 EvalDiffuse(vec3 wi, vec3 wo, vec2 uv) {vec3 albedo GetGBufferDiffus…

【Linux】基本认知全套入门

目录 Linux简介 Linux发行版本 发行版选择建议 Centos-社区企业操作系统 Centos版本选择 Linux系统目录 Linux常用命令 SSH客户端 Linux文件操作命令 vim重要快捷键 应用下载与安装 netstat,ps与kill命令使用 Linux应用服务化 Linux用户与权限 Linu…

接口自动化测试实战

测试前准备: 1、项目的介绍 是一个什么项目、项目技术、项目要测的接口和业务流程、业务路径测试用例(通过业务流程来梳理业务路径) 2、链接和登录密码: 客达天下http://huike-crm.itheima.net/#/clue 客达天下账号admin&…

支持向量机-笔记

支持向量机(Support Vector Machine, SVM) 是一种强大的监督学习算法,广泛应用于分类和回归任务,特别是在分类问题中表现优异。SVM 的核心思想是通过寻找一个最优超平面,将不同类别的数据点进行分割,并最大…

【YOLO学习】YOLOv4详解

文章目录 1. 整体网络结构1.1 结构图1.2 创新点概括 2. 输入端创新点2.1 Mosaic数据增强2.2 cmBN策略 3. Backbone创新点3.1 CSPDarknet533.2 Mish函数3.3 Dropblock正则化 4. Neck创新点4.1 SPP模块4.2 PAN 5. Prediction5.1 Loss5.2 NMS 1. 整体网络结构 1.1 结构图 1.2 创新…

PostgreSQL学习笔记三:数据类型和运算符

数据类型和运算符 PostgreSQL 支持多种数据类型和运算符,以下是一些常见的数据类型和运算符的概述: 数据类型 基本数据类型 整数类型: SMALLINT:2 字节,范围 -32,768 到 32,767。INTEGER:4 字节&#xff0…

vue3 vue2

vue3.0是如何变快的? diff算法优化 vue2的虚拟dom是进行全局的对比。vue3 新增了静态标记(patchFlag) 在与上次虚拟节点进行比较的时候,只对比带有patch Flag的节点,并且可以通过flag的信息得知当前节点要对比的具体内…

先进封装技术 Part03---重布线层(RDL)的科普

先进封装核心技术之一:重布线层(RDL)的科普文章 1、 引言 随着电子设备向更小型化、更高性能的方向发展,传统的芯片互连技术已经无法满足日益增长的需求。在这样的背景下,RDL(Re-distributed Layer,重布线层)技术应运而生,成为先进封装技术中的核心之一。 2、 RDL技术…

yolov8.yaml

前面说了yolov8的核心代码放在ultralytics里面,今天我们一起学习一下 YOLOv8模型下的Ultralytics文件目录结构。每个文件夹都有不同的作用,以下是对各个文件夹的解释: assets: 这个文件夹通常存放与模型相关的资源文件,可能包括训…

MySQL五千万大表查询优化实战

背景 DBA同事在钉钉发了两张告警截图,作为“始作俑者”的我很心虚,因为刚才是我在管理后台查询数据,结果很久都没出来,并且用多个维度查了N次 问题分析 这是当天上线的功能,完事我立马锁定SQL然后开启排查 # 原SQL&a…

系统性能优化

在程序员的职业生涯中,解决当前系统问题,优化性能,是走向高阶的必经之路。如果一辈子做着后台开发,写着CRUD,QPS低于10,那确实没必要去做性能优化,因为根本用不上。性能优化范围很广&#xff0c…

排序|插入排序|希尔排序|直接选择排序|堆排序的实现即特性(C)

插入排序 基本思想 直接插入排序是一种简单的插入排序法,其基本思想是: 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。 单趟 当插入第 i ( i ≤ 1…