【Linux】多线程相关第一篇:从进程谈起理解线程概念

news2024/10/4 5:56:30

文章目录

  • 为什么需要线程
  • 初步认识Linux线程
  • Linux操作系统的线程为什么要这么设计
  • 进程、线程关系梳理
  • 理解线程是CPU调度的基本单位
  • 简单认识多执行流如何划分代码

为什么需要线程

线程和进程的关系密不可分。

操作系统教材对于进程、线程的概念是这样描述的:

  • 进程是被加载到内存的程序,是承担分配系统资源的基本实体。
  • 线程是进程内部的执行分支,是CPU调度的基本单位。

很抽象,很不好理解,但是能够肯定的是,线程和进程有莫大关联,所以我们可以以进程为切入点来理解线程。

第一点,理解什么是进程

“进程是被加载到内存的程序”,这样的说法肯定是没错的,但是不够详细、不好理解,深入一点的话,我们可以这样来理解:进程 = 内核数据结构 + 程序代码跟数据。

解释如下:
首先,我们得有这样的一个认识,程序只是一个磁盘文件,它是我们写的代码经过编译器编译链接操作后生成的一个文件,它的内容包括二进制指令以及执行这些指令所需要的数据。在冯·诺依曼计算机体系结构中,CPU只与内存直接交互,磁盘属于外设,程序又存储在磁盘上,所以CPU无法获取到程序的指令和数据,程序要被运行起来就得先被CPU“看”到,然后CPU才能执行程序指令,这就要求操作系统先把程序拷贝到内存中,这个“拷贝”的过程就叫做“加载”。

其次,我们在计算机上可以启动很多的程序,这就意味着操作系统会把很多个程序文件拷贝到内存,内存上同时存在很多个被拷贝进来的程序文件,操作系统是计算机资源的管理者,它要将这些程序文件管理起来,管理分为两个步骤:先描述,再组织。

描述指的是操作系统内部会有一堆相关的结构体来记录内存级程序的各种信息,比如说进程PCB、进程地址空间、页表、文件描述符表等;组织指的是操作系统会将基于结构体创建的对象,通过顺序表、链表、队列等某些数据结构的形式组织起来,由于这是在操作系统内核层面上发生的,所以又称内核数据结构。

然后我们就能够得到这么一张结构图:
在这里插入图片描述

CPU要执行进程代码(指令),就可以通过进程控制块(PCB)访问进程地址空间正文代码区域中的虚拟地址,在经过页表转化之后就可以找到物理内存上的代码跟数据。

第二点,理解什么是多执行流

进程的源代码由程序员编写,其中包含了许多函数。这些函数在编译之后会被转换成指令块,每个指令块都有自己的入口地址,这个入口地址对应着原来的函数名。假设有一个进程A,它的源代码经过编译后包含了100个函数。当CPU调度执行进程A的代码时,它会从main函数开始执行,然后依次调用function1()、function2()、function3(),直到最后一个函数function99()被执行完毕。最终,main函数返回,整个进程的代码被CPU串行调度完成。

在A进程代码被CPU执行的过程中,我们发现任意时刻只有一个函数(或指令块)在被执行。像这种从执行开始到执行结束中不会存在两个函数同时被执行的过程称之为一个执行流。

理解到这里之后,我们当前可以简单认为一个进程就是一个执行流(后续被修正)。

假设现在又有一个程序,它需要处理大量数据。在单个进程中,程序需要依次处理每个数据块,这可能会导致处理时间较长。然而,如果我们将这个程序拆分成多个进程,每个进程负责处理数据的不同部分,那么这些进程就可以并行执行,每个进程独立地处理数据,将数据处理完之后,通过进程间通信,交还回给一个主进程,最终得出结果,从而加快整体处理速度。这种情况下,每个进程都代表了一个独立的执行流,它们同时在执行,实现了多执行流的效果。

我们希望多进程存在的目的不是为了实现多进程本身,而是为了多执行流并发执行进程代码从而提高运行效率。

第三点,多进程并发实现多执行流的时空成本极高

多进程并发实现多执行流这个方案不是完美的,它有一个缺点就是时间空间成本消耗极大。

以父子进程为例,由于进程具有独立性,这个独立性表现在,当父进程创建子进程时,操作系统会为子进程也创建出属于子进程自身的周边内核数据结构,还会用父进程的内核数据结构中的数据初始化子进程的内核数据结构,父子进程间代码共享,数据写时拷贝。

首先说时间成本,进程相关的内核数据结构是非常多的,这里只是选取的几个经典结构来举例,创建内核数据结构需要时间,初始化内核数据也需要时间,进程越多,准备工作所需要的时间就越大,达到一定程度就是,就会抵消掉多进程并发的优势。

然后是空间成本,内存的容量是有限的,进程越多意味着内核数据结构以及执行进程代码所需数据占据的内存空间就会越来越大。

在这里插入图片描述

所以,Linux就需要一种新的、代价更加小的方式来实现多执行流并发执行进程代码的技术,这个技术就是线程!

初步认识Linux线程

多进程并发实现多执行流的痛点在于进程的创建和初始化会带来额外的开销,在进程具有独立性的前提下,Linux必须确保每个进程都能够正常运行而不受其他进程的影响,因此,即使是相对简单的进程,也需要进行一系列的初始化操作,包括建立进程表、分配内存、加载可执行文件等。这些操作需要耗费时间和资源,并且可能会导致系统性能下降。

所以,Linux线程技术之一的目标是降低进程创建和初始化的开销。

关于进程地址空间,我们可以换一种角度来理解它,我们可以把进程地址空间及其内部的地址当成是一种资源。为什么这么说?

一个进程执行需要很多资源,如代码、数据、库、参数等,进程查找资源的时候都是通过进程地址空间来查找的,进程地址空间上的每一种资源都会有一个虚拟地址来作为唯一标识,可能是一行代码,可能是某个数据,可能是某个系统调用的入口,进程只要获得了一个合法的虚拟地址,经过页表的映射访问物理内存就能够找到对应的资源。

如果从资源的角度看待进程地址空间,进程地址空间是资源就意味着它在进程内部很多资源都是可共享、按需利用的,我们的目的不是要将一个执行流的进程代码拆分成多个执行流吗,之前是通过创建多个进行来完成这一操作,现在我们可以做这样的一件事,假设现在需要n个执行流,我们就创建n个进程PCB,这n个进程PCB都在一个进程内部,它们“看”到的都是同一个进程地址空间,然后通过某种方法,让这n个进程PCB执行地址空间中正文代码的某一部分,通过这样方式,原本进程内部只有一个执行流,现在进程内部就存在多个执行流,每个进程PCB就代表着一个执行流。

再回顾一下操作系统教材上的表述,“线程是进程内部的一个执行分支”,因此,进程内的一个进程PCB就象征着一个Linux“线程”。

在这里插入图片描述

Linux操作系统的线程为什么要这么设计

“线程和进程一样都是要被CPU调度的,线程是进程内部的一个执行分支,是CPU调度的基本单位”,这是操作系统学科告诉我们的概念,但是它没有告诉我们怎样做才能让线程在进程内部运行,怎样做才能让线程成为CPU调度的基本单位。

正式因为操作系统学科只谈方法论,不谈具体实现,换句话来说,只要实现的效果能够满足要求,不管操作系统内核的底层是如何实现的,只要能够遵守操作系统方法论中的这个概念,那它就是“线程”!

按照操作系统学科的描述,一般情况下,线程采取先描述,再组织的方式实现。

线程是进程内部的一个执行分支,一个进程内部可能会存在多个线程。一个操作系统运行起来会有很多进程在被调度,这就意味着,操作系统内部会存在比进程数量更多的线程。

那OS要不要创建线程呢?要不要按优先级调度线程呢?要不要对线程的上下文进行保存完成切换呢?一个线程执行完毕操作系统要不要对这个线程进行回收呢?说这么多的目的就一个,如果要在操作系统实现线程,就要对线程进行管理,就如同操作系统对进程做管理一样。

怎么管理,先描述,再组织。

和描述进程的PCB(Process Control Block,进程控制块)一样,操作系统内部也会存在一个叫做线程TCB(Thread Control Block,线程控制块)的内核数据结构来描述线程。如果说一个进程内部存在五个线程,那么操作系统就会分别为这五个线程创建TCP对象,然后通过某种数据结构统一管理起来,假如说这个数据结构是链表,那么,对线程的管理就从概念变成了对链表的增删查改,然后每个进程的PCB内部都会有一个指针指向属于自己的线程列表。线程TCP内也会有很多的用于描述线程属性的成员变量,比如说描述线程唯一标识符的,描述线程优先级的,描述线程上下文状态的……等等。

但实际上发现,如果真的这么去设计的话,整个操作系统就会变得非常复杂,且代码冗余,为什么这么说?

第一,线程和进程的管理操作非常相似,它们都需要操作系统进行调度、创建、终止等操作,因此线程和进程的控制块会有很多重合的属性;第二,也是更重要的,操作系统内部已经有了一套进程调度算法,如果线程被设计出来后,就意味着操作系统实现者就得去为线程再设计一套新的调度算法,操作系统在实际运作的时候就得先执行进程调度算法,再执行线程调度算法,这是不是有点太麻烦了?

在这里插入图片描述

综上,LInux的设计者认为,线程和进程都是执行流,二者具有高度的相似性,没有必要单独为线程设计数据结构与算法,能够直接复用进程代码,使用进程模拟线程。

这种通过复用进程代码来实现线程的方案带来了几个明显的好处:

  1. 简化系统设计和开发成本: 由于线程和进程具有高度相似性,因此通过复用进程代码来实现线程可以大大简化系统的设计和开发成本。不需要额外设计和实现线程管理、调度等功能,避免了重复造轮子的工作。

  2. 减少内核开销: 操作系统内核需要为每个进程和线程维护一些数据结构和元数据,如进程控制块(PCB)和线程控制块(TCB)。通过复用进程代码,可以减少内核开销,因为不需要为线程额外维护和管理独立的数据结构,而是直接利用了进程已有的数据结构。

  3. 统一管理: 通过将线程实现为进程的一部分,可以统一管理进程和线程,简化了操作系统的内部逻辑。这样一来,进程和线程之间的关系更加清晰,管理和调度也更加统一和一致。

  4. 更好的可移植性: 由于Linux系统已经实现了进程管理的功能,并且大多数操作系统都支持进程管理,因此基于进程的线程实现方案具有更好的可移植性。开发人员可以更容易地在不同的操作系统之间迁移和部署他们的应用程序。

进程、线程关系梳理

有了上面的了解之后,再来梳理一下进程和线程的概念和它们之间的关系。

线程是进程内部的执行分支,一个进程内部有多少个进程PCB就有多少个执行分支,即执行流(当然,这不代表着进程PCB就是线程,只是说一个进程PCB象征着一个线程)。

进程 = 内核数据结构 + 进程代码数据,在没有了解线程之前,我们一般会认为一个进程PCB代表着一个进程,但是现在要摒弃这个观点,因为没有谁规定一个进程内部就只能有一个进程PCB。不过现在,我们可以从内核角度来理解进程,即进程是承担分配系统资源的基本实体。举个例子来说,一个进程整体就像是一个小盒子,内存中占用了一块空间,盒子内的空间,分割成一个一个的小块,分别分给执行流、进程地址空间、页表、代码数据资源等。
在这里插入图片描述

理解线程是CPU调度的基本单位

从资源的角度来理解,我们能很好地对进程、线程做区分,可是如果是从调度的角度来理解,我们发现进程和线程之间的关系又变得模糊起来,进程能被调度、线程也能被调度、二者之间该怎么做区分?

在Linux中,无论是进程还是线程,它们都代表着一个执行流,都可以被CPU调度执行。过去,当进程只有一个执行流时,操作系统会将进程的控制块(PCB)链入CPU的等待队列,一旦轮到该进程,CPU就会调度该进程执行进程代码。但现在,由于一个进程内部可能有多个执行流,那么CPU是否需要区分这些执行流呢?

实际上,Linux内核并不区分进程和线程的调度。无论是进程还是线程的PCB,它们都能够访问进程地址空间中的代码和数据,都能够访问完成执行所需的所有资源。因此,站在CPU的角度,它们都是可以被完整执行的。在Linux中,不区分进程和线程,它们都是执行流的代表,这就足够了。

所以关于进程和线程,就存在这样一个关系:线程 ≤ 执行流 ≤ 进程

同时这里就再引出一个知识点,在Linux操作系统中,其实是没有 “线程” 这个概念的,因为Linux中没有一个真正意义上的独立的线程数据结构(TCB),所以 “线程” 这个说法只是为了与操作系统学科相结合,因此在Linux中只有两个东西,一个是进程,一个是进程内部的执行流,而对于执行流,它有一个正式的称呼,叫做 “轻量级进程”。

简单认识多执行流如何划分代码

一个进程的地址空间被所有进程PCB共享,所以按道理说每一个进程PCB都能看到完整的代码,那是怎么做到让一个进程PCB只看到一部分进程代码的,答案是页表!

下面就以32位平台为例,了解一下页表的结构。

众所周知,在 32 位平台下有 2 32 2^{32} 232 个地址,这就代表着有 2 32 2^{32} 232 个地址需要完成从虚拟地址到物理地址的映射。

一般来说,我们印象中的页表是长这样子的:
在这里插入图片描述

我们可以来算一下这样一张页表在内存要占用多少内存,页表中的一行有两个地址,一个地址占4字节,页表一行就占用8字节,32 位平台下,一个地址大小为4字节,有 2 32 2^{32} 232 个地址,总共占用的空间大小为 8 × 2 32 ≈ 2 35  字节 ≈ 16  GB 8 \times 2^{32} \approx 2^{35} \text{ 字节} \approx 16 \text{ GB} 8×232235 字节16 GB

32 位平台下总的内存容量才 4GB,实现一张页表却要 16GB,这多少有点离谱了,所以页表真正的结构肯定不是像上面这样子的,实际上一张完成的页表由一堆表构成,被称为 “二级页表”。

第一级页表(又称“页目录”):指针数组,包含指向第二级页表的指针。
第二级页表:指针数组,包含指向物理页框的指针。
页框:一个 4KB 大小的内存块。操作系统会将内存划分为 4KB 大小的内存块然后统一管理起来,简单来说就是可以理解为,内存就是一个超级大的数组,sizeof(这个数组中的一个元素) 得到的结果是 4KB,这个数组有 ( 4 G B / 4 K B = 1 , 048 , 576 ) (4GB / 4 KB = 1, 048, 576) (4GB/4KB=1,048,576) 个 元素。

对于一个从地址空间得到的虚拟地址,32位平台下虚拟地址转换成2进程有32个比特位,从虚拟地址到物理地址的映射过程如下:

  1. 访问虚拟地址的前10个比特位,在页目录找到对应的页表。
  2. 再选择虚拟地址的中间10个比特位,在页表中找到页框的物理起始地址。
  3. 将最后12个比特位作为偏移量从页框对应地址处向后偏移,找到物理内存中的某一个对应的字节数据, 2 12 2^{12} 212 刚好等于 4 KB。。

在这里插入图片描述

然后可以来算一下,这种结构下的页表实际占多少内存空间,一张页表就是一个指针数组,指针大小 4 字节,数组有 1024 个元素,一张页表总共消耗的内存为 4KB,一共有 1025 张页表(加上页目录),一个进程只需要不到 5M 的固定内存消耗,就能够完成虚拟地址到物理地址的映射。

回到一开始的问题,操作系统是怎么让不同的线程看到不同的进程代码的,这个其实在图中也体现出来了,可执行程序 hello.exe 被加载到内存中消耗 5 个页框,这 5 个页框就是进程的代码和数据,现在假设进程 hello.exe 内有三个线程,只需要让它们各自看到不同的二级页表,那它们就可以各自看到一部分的进程代码和数据,线程在被调度时就可以无干扰执行进程代码。

以上是线程概念相关的基本内容,有理解不到位的地方还请多多指出。
有理论就有实践,接下来要了解的就是线程空间相关的内容,即是编写代码实现线程的创建、终止、分离等操作,这里就放到下一篇文章中。

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

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

相关文章

Python-VBA函数之旅-super函数

目录 一、super函数的常见应用场景 二、super函数使用注意事项 三、如何用好super函数? 1、super函数: 1-1、Python: 1-2、VBA: 2、推荐阅读: 个人主页: https://myelsa1024.blog.csdn.net/ 一、su…

JS解密之新js加密实战(二)

前言 上次发了一篇关于新加密的,只解了前边两层,这中间家里各种事情因素影响,没有继续进一步研究,今天百忙之中抽空发布第二篇,关于其中的一小段加密片段,我认为分割成多个小片段是更容易被理解的。逻辑相…

【SRC实战】修改赠送金额支付漏洞

挖个洞先 https://mp.weixin.qq.com/s/NQKJQF81XpG8815EfgvgKw “ 以下漏洞均为实验靶场,如有雷同,纯属巧合 ” 01 — 漏洞证明 “ 充值赠送金额能否修改? ” 1、充值30元赠送1.9元礼包,充值100元赠送7元礼包,充值…

买卖股票的最佳时机 II(LeetCode 122)

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容,和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣! 推荐:数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航: LeetCode解锁100…

FANUC机器人单轴零点标定时提示无法执行零点标定,由于重力补偿已启用,所有机器人轴的脉冲计数必须有效

FANUC机器人单轴零点标定时提示无法执行零点标定,由于重力补偿已启用,所有机器人轴的脉冲计数必须有效 首先,机器人由于长时间断电未使用,6个轴的编码器数据全部丢失,上电后报警SRVO-062, 有关SRVO-062故障报警的相关内容可参考以下链接: FANUC机器人SRVO-062报警原因分…

通过集成式 PLM Services for SIMULIA 实现协作

在快速发展的产品开发世界中,无缝和高效的管理解决方案已成为必需品。在这些解决方案中,PLM 服务正变得越来越普及,这要归功于它们的能力。这些服务提供了一种管理产品生命周期的集成方法,从概念开始,到设计和制造&…

Pyhton专题学习资料包,Python从入门到精通全套学习资料[30G]

资源概览 百本Python学习书籍大礼包百本前端学习书籍大礼包微专业-数据挖掘分析之Python篇小甲鱼零基础入门学习Python(全96集) 资源获取 🧑‍💻【Pyhton专题资料】【30G】 百本Python书籍## 百本前端书籍 微专业-数据挖掘分析之Python篇 预备课【先…

Docker下Open WebUI,Ollama的安装实践

提示一下Open WebUI与ollama的关系。后端的同学可以理解为Open WebUI等于是个Navicat,Ollama就是具体的数据库实例。 官方安装文档: 🏡 Home | Open WebUI Open WebUI官网文档翻译: 注意: 使用Docker安装Open WebU…

有趣的css - 打字机动画效果

大家好,我是 Just,这里是「设计师工作日常」,今天分享的是使用 css 实现好玩的单行打字机效果,和我一起看看吧。 《有趣的css》系列最新实例通过公众号「设计师工作日常」发布。 目录 整体效果核心代码html 代码css 部分代码 完整…

成员函数构造函数析构函数

文章目录 类的6个默认成员函数构造函数概述定义特性 析构函数概述特性 类的6个默认成员函数 空类: 如果一个类里面什么都没有写,我们称之为空类 class Date {};空类真的什么都没有吗? 实际上并非如此,编译器会自动生成6个默认成…

樊春海院士/姚广保课题组2024年博士后/助理研究员火热招募!

尊敬的读者们,我们很高兴地向您介绍一个激动人心的机会——上海交通大学张江高等研究院正在进行博士后和科研助理的招聘,这是一个与顶尖科学家共事、参与前沿科学研究的绝佳机会。 工作地点位于风景优美、充满活力的上海市浦东新区,这里是中国…

FENDI CLUB精酿啤酒馆与传统啤酒销售模式的不同

精酿啤酒火了,国产品牌精酿也在迅速崛起,为精酿啤酒这一小众品类发展加足了马力。与此同时,精酿酒吧、精酿小酒馆也开始出现了增长。这标志着中国精酿啤酒市场的快速发展和国产品牌的崭新局面。 FENDI CLUB精酿啤酒已经在不少地方开始积极开…

CDGA|数据治理实战案例:从数据收集到治理,再到价值应用

在当今信息爆炸的时代,数据治理已成为企业提升核心竞争力、实现数字化转型的关键一环。本文将通过一个实战案例,详细剖析数据治理的全过程,从数据收集到治理,再到价值应用,为读者提供有益的参考和启示。 数据收集&…

港中深「户外自重构蜗牛机器人集群」登Nature子刊!

在科幻电影《超能陆战队》中,我们见证了一种由成千上万个微小磁性单元组成的机器人通过磁力相互连接,形成各种复杂的三维结构。香港中文大学(深圳)林天麟教授团队致力于将这一科幻转化为现实,近年来开发了一系列自由形…

APP反抓包 - 客户端证书验证

一,校验的原理 下图为HTTP协议的请求过程:传输过程中都是明文数据 下图为HTTPS协议的请求过程: 注意:公钥加密的数据只能通过对应的私钥才能解密,就算是进行加密的公钥也不能进行解密。 上述的请求过程看似复杂,实际就是两部分: 通过公钥与私钥同步对称密钥使用对…

安科瑞工业IT产品及解决方案—电源不接地,设备外壳接地【监测系统对地绝缘电阻】

低压配电系统分类及接地保护方案 国际电工委员会(iec)对各接地方式供电系统的规定规定:(低压:交流1000V以下) 低压配电接地、接零系统分为IT、TT、TN三种基本形式。TN分为TN-C,TN-S,TN-C-S三种…

网络编程UDP

目录 1.什么是网络编程 1.1发送端和接收端 1.2请求和响应 1.3客户端和服务端 1.4常见的客户端服务端模型 2.Socket套接字 2.1Socket概念 2.2三种Socket套接字分类 3.Java数据报套接字通信模型(UDP) 4.Socket编程注意事项 5.UDP数据报套接字编程…

只需三步,教你轻松搞定内网穿透

最近开发过程中又遇到了需要外网访问内部服务接口的需求,比如调用三方服务的各种回调通知、支付成功回调、大模型回调等都需要外部服务器来访问内部的接口,这里有个问题就是如果我们在本地或者测试环境调试的过程中我们使用的是内网环境,那外…

【数据结构课程学习】:队列学习

🎁个人主页:我们的五年 🔍系列专栏:数据结构课程学习 🌷追光的人,终会万丈光芒 🎉欢迎大家点赞👍评论📝收藏⭐文章 目录 🚗 1.队列的基本概念&#xff1a…

AI地名故事:笔岗村

笔岗村,实际上是由笔村和宏岗村两个古老的村落合并而成的。南宋度宗元年,也就是公元1265年,笔村开始建立。随着时间的推移,到了宋代后期,宏岗村也相继建立。这两个村落各自承载着丰富的历史和文化,最终在历…