Linux——线程(1)线程概念与控制

news2025/7/16 8:10:43

线程?这个名字我们似乎有些眼熟?没错,我们之前提到过的进程和这个有点像。但进程和线程有什么关系呢?本系列我们讲从线程的概念出发,了解一下Linux中的线程以及线程和进程的关系等内容。

一、线程的概念

线程是一个执行流,执行力度比进程要更加细,是进程内部的一个执行分支,是进程中实际运作单位。也就是说,一个进程可以有多个线程,那么操作系统如果要支持线程,就必须对当前的线程进行管理!——先描述再组织。进程对应的我们有PCB,线程我们有TCB。 

但是我们一想,一个进程有一个PCB,但同时会有多个tcb,一个task_struct要连接这么多的结构,是不是有些太麻烦了?因此,操作系统直接把pcb看成是tcb,也就是说,当我们的进程创建线程时与进程的task_struct共享一份地址空间,然后把代码区分成若干份分别指向不同的线程执行。

也就是说,线程是靠进程模拟实现的。我们把图片左侧的task_struct和红框部分的线程统一称为执行流,其中一个执行流我们称线程,只有task_struct+地址空间+页表称为进程。我们之前的进程系列的进程也算是进程,只不过只有一个执行分支。(线程)在Linux中,我们把执行流统一称为轻量级进程(LWP)。

二、线程的基本模拟

上图是线程创建的接口(稍后会对参数进行解释)

以下是演示代码

执行流程是,创建了新线程后,之前的执行流会继续向下执行,而新创建的执行流会去执行上面的run函数。但我们编译后发现居然发生了链接报错。

其实,pthread_create并不是一个系统调用,而是glibc封装的一个原生线程库,解决这个问题我们只需要在编译选项后加 -lpthread即可

运行结果也的确像我们说的

我们通过get两个线程的pid发现与进程的pid相同,也说明了来自于同一个进程

除此之外,我们也可以查询线程的状态,进程的状态我们说过用 ps axj |  grep processname

查询线程的状态:
 

ps -aL | grep processname

在众多的执行流中,pid与LWP相同的是主线程。

我们讲过,线程没必要再创建一个tcb,只要和进程共享一个pcb即可,那么也就不需要给线程执行的代码开辟额外空间,只需要把要执行的函数起始地址交给某执行流即可(函数还是在pcb内)

三、进程与线程

进程是资源分配的基本单位,而线程是调度的基本单位

此外,线程虽然共享进程的数据,但也拥有自己的一部分数据,比如线程id,上下文,调度优先级等。

在文件方面,同一进程的多个线程是共享的,也就是说如果一个线程打开了某个文件,那么其他线程也能看到该文件的内容。包括信号的处理方式等。

在一个进程中,如果有一个线程发生了崩溃,那么整个进程也将崩溃。

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。

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

四、关于虚拟地址与页表的补充

1.虚拟地址和页表的由来

如果没有这两个的分配内存机制,试想一下,我们每一个用户在使用内存时在空间上必定是连续的

因为每个程序的代码的数据大小不同,所以对应所占的空间大小不同,因此,物理内存会被分成若干个大小不同的块(如上图),导致相同的数据类型放在不同的各个区域。此时如果有些程序要推出的话,所占的内存就会被回收,导致这些内存被碎片化。

我们希望改变这种现状,但又不想改变用户使用空间连续的习惯(即用户的空间可以连续但物理内存不要连续),所以才有了虚拟地址空间和页表

图中有一个名词:页框,其本意就是把物理内存按照固定大小进行分隔的块,最常见的是4KB,而每个页框中存放一个页,大小就是一个页框的大小。(页是数据块,页框是一个存储区域)。这种机制就保证了CPU并非直接访问物理内存,而是通过虚拟地址来间接访问,也就是通过页表,页表上记录了页与页框的对应关系。

2.物理内存的管理

现在我们知道了,一大块物理内存被分割成无数的页(数据块),我们假设每个大小是4KB,假设整个内存是4GB,那么就要有4GB/4KB=100w+个页框。这么多个页框的内存,OS必定要管理,那么就会有对应的结构体来描述组织。而在结构内部我们发现我们发现有类似与数组下标的结构struct page *mem_map[N],可以大胆猜测,虽然物理内存被分割成无数块,但实际上他们还是连续的,那么我们就可以把每一个页框标记一个数组下标,整个内存就是一个巨大的数组,要想找到对应的内存,页表内只需记录映射到内存的下标就可以了!

3.页表的真实面目

目前我们对页表的了解,也就只知道它可以帮助我们映射到物理内存,但其内部还存放着其他信息。

我们之前了解的页表结构并不严谨,试想一下,如果把4GB的物理内存分成若干个4KB的块,然后把虚拟地址的每一块(每个地址是4字节)都记录在内形成映射关系以及对应的物理内存,那么单看页表的大小就要达到16GB左右了,这显然是不合理的。

其实,从虚拟地址到物理地址的转化并不只有页表,还有一个页目录,页目录中有1024个页目录表项,而每个表项对应着一个页表,然后每个页表中有1024个页表项,每个页表项指向物理地址的每一块起始地址,(所有页表的大小就是1024*1024*4=4MB),这样我们用4MB大小就可以映射到整个物理内存了

这里的每⼀个表,就是真正的页表,所以⼀共有 1024 个页表。⼀个页表自身占用 4KB,那么1024个页表就占用4MB的物理内存空间,和之前没差别啊?那么 从总数上看是这样,但是⼀个应用程序是不可能完全使用全部的4GB空间的,也许只要几十个页表就 可以了。例如:一个用户程序的代码段、数据段、栈段,⼀共就需要 10 MB 的空间,那么使用 3 个页表就足够了。 计算过程: 每⼀个页表项指向⼀个4KB的物理页,那么⼀个页表中1024个页表项,⼀共能覆盖4MB的物理内存; 那么10MB的程序,向上对齐取整之后(4MB的倍数,就是12MB),就需要3个页表就可以了。

把1024个页表管理起来的就是页目录,每个页目录表项的大小也是4字节。

4.虚拟地址如何转化为物理地址

我们的虚拟地址,一般是由32个比特位组成的,代表要访问某一个字节的地址(不是块的地址),我们把前10个比特位去查页目录表项(1024个,拿下标去查找到对应的页表),再拿次10位在对应的页表下找对应的页表项,然后我们就找到了页框的起始地址。而剩下的12位就是偏移量,利用偏移量就能找到该块中的某一个字节(2的12次就是4096,每个块是4kb*1024=4096字节)。

这个转化过程是MMU硬件完成的(CPU中)。然后再通过CR3寄存器查表找到对应的物理地址。

多级页表给我提供了便利的同时也引入了新的问题——查找次数增多导致效率降低,需要提升效率。在CPU中,还真有一个东西帮助我们——TLB,MMU在查页表前先问问TLB有没有,如果有就直接拿到物理地址,但没有的话就只能查表,然后把地址缓存到TLB方便下次查询,TLB的本质就是缓存。

5.缺页中断

我们的页表中记录的地址其实还记录了有关地址权限的相关信息(RW),假设现在有一个只读的地址,我此时进行写操作,此时MMU在查询地址时就发现不对,就会把错误告诉CPU发生软中断进行错误处理。同时,我们的数据并不用同时加载到内存里,其实一次加载一定部分就可以,等要执行下面的内容再进行加载(也说明了页表虽多但不都用)。

五、线程的控制

1.接口介绍

(1)pthread_create

上面提到过,这并不是系统调用而是一个库,是用户级别的线程库。

参数 :

thread: 返回线程 ID   (输出型)

attr: 设置线程的属性, attr 为 NULL 表示使用默认属性

start_routine: 是个函数地址,线程启动后要执行的函数

arg: 传给线程启动函数的参数

arg参数可以是任意类型(变量,数字,对象等) 

成功返回0,失败返回错误码。 

(2)pthread_self

用于获取tid,哪个线程调用就获取哪个tid。

我们也可以同时多个线程执行同一个函数,但如果不加保护的情况下,会发生数据错乱(类似于多态??)此外,进程内的函数,全局变量,线程也是共享的。

(3)线程等待——pthread_join

等待哪个线程就输入哪个LWP,至于第二个参数,是一个二级指针(输出型),原理和进程类似,只要等待线程不退出,主线程就会阻塞等待。(返回值等于0等待成功)

第二个参数一般用于接收线程执行的函数返回值,既然是一个二级指针,我们就需要用一个void*的变量地址传参。

相当于把10写进了ret。(我们要把return的值强转位void*)

(4) 线程终止

exit():其实在此并不常用,因为exit放在任何位置,只要触发就表示进程退出,所以如果只想让某一线程退出就不能使用这个。用pthread_exit。

等价于return。

还有一种退出方法

线程可以被取消

2.线程分离

当主线程正在做某些事情时,某个线程要退出,就需要阻塞等待,但如果我们要让主线程做自己的 事情不用等待呢?——把目标线程进行分离(joined:默认要等待,detach:分离态)

接口:pthread_detach(pthread_t thread)

用法通俗易懂,有一点,线程把自己分离需要传的是pthread_self()。一般要分离的线程是我不关心线程的返回值,不需要去等待,等线程退出自动释放资源。

 六、关于线程ID

获取线程id可以用接口也可以用输出型参数带出,我们发现线程ID都是很大的一串数字,我们把其转成16进制就是这样的:

有点眼熟?好像地址啊,的确就是地址!那线程的ID为什么要是地址呢,这个地址的意义在哪里?

我们知道,一个带线程的可执行程序是要第三方库线程库支持的,那么当程序执行时库也要加载(加载到内存空间的共享区以让task_struct看见)。

但对于Linux,没有线程这个概念,只有统一的LWP,用户要用线程,但系统只有创建LWP的接口,库中给我们提供相关线程的接口,同时我们也要获取线程的相关信息(id,优先级等),这就需要库中给我们维护,当我们创建一个线程就可以填充相关属性。用库进行封装,我们就可以避开系统调用,直接去库找就可以了。

库中的每一个线程的tcb可以看成是数组存放

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

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

相关文章

备忘录模式:实现对象状态撤销与恢复的设计模式

备忘录模式:实现对象状态撤销与恢复的设计模式 一、模式核心:在不破坏封装性的前提下保存和恢复对象状态 在软件开发中,经常需要实现 “撤销” 功能(如文本编辑器的撤销修改、游戏存档读取)。直接暴露对象内部状态会…

freecad参数化三维模型装配体解析至web端,切换参数组或修改参数

用免费开源的freecad制作全参数化的三维模型,并且装配,上传至服务器,解析至web端,用户可以切换参数或修改参数,驱动模型改变。 freecad全参数化装配体模型解析至web端进行参数切换、修改完整展示_哔哩哔哩_bilibili …

浅析锁的应用与场景

锁的应用与场景:从单机到分布式 摘要:在多线程和分布式系统中,“锁”是避免资源竞争、保障数据一致性的核心机制。但你真的了解锁吗?什么时候该用锁?用哪种锁?本文通过通俗的比喻和代码示例,带…

语音合成之五语音合成中的“一对多”问题主流模型解决方案分析

语音合成中的“一对多”问题主流模型解决方案分析 引言“一对多”指的是什么?优秀开源模型的方法CosyvoiceSparkTTSLlaSA TTSVITS 引言 TTS系统旨在模仿人类的自然语音,但其核心面临着一个固有的挑战,即“一对多”问题 。这意味着对于给定的…

ElementUi的Dropdown下拉菜单的详细介绍及使用

Dropdown是 ElementUI 中用于创建下拉菜单项的一个组件,通常el-dropdown-item 包裹在 el-dropdown 组件中使用。以下从功能特性(一些属性及方法)、使用和高级功能(高亮显示,滚动,额外传参数)三个方面进行详细介绍。 一、功能特性 1.触发方式…

Linux麒麟 V10 系统找回 root 密码的步骤

Linux麒麟 V10 系统找回 root 密码的步骤 1 环境介绍2 操作步骤2.1重启系统并进入 GRUB 菜单2.2 输入 GRUB 账户密码2.3 修改启动参数2.4 启动系统2.5 修改root 密码2.6 重启系统 3 Linux命令全方位指南实战教程Linux命令学习使用列表 1 环境介绍 有时候root 密码忘记&#xf…

stone 3d v3.3.0版本发布,含时间线和连接器等新功能

1.新加了时间线(timeline)编辑器,可以类似blender一样给对象制作动画 2.新加了度量(metrics)系统,通过scene对象检测器中的useMetrics属性来启用或禁用,启用时所选物体将显示三维度量数据 新加了…

.whl文件

本文主要介绍了.whl文件的定义,怎么安装.whl文件(离线,在线)。 怎么查看cuda的版本,以及如何安装相应版本的cuda(本地电脑,超算上) 以及如何创建.whl文件 .whl文件的定义 Document…

Git命令行中vim的操作

Git命令行用vim打开文件,或者用其他git命令打开了文件,需要编辑和保存文件等,有些命令表情奇怪,往往容易忘记这些命令。记录下。 下面这篇比较实用和简练: gitvim编辑文件命令 • Worktile社区https://worktile.com/…

C#初级知识总结

一、什么是CIL 1.CIL(Common Intermidate Language)是指.Net的公共中间语言,它是一种编程语言。 .Net框架的各种语言在编译时都会编译成同一种中间语言(CIL),之后程序运行的时候CIL会被JIT(Just In Time)转换为二进制语言&#xf…

Linux学习笔记之环境变量

写这篇博客的目的主要是因为本人学习动静态库时,用到了环境变量的知识,发现略有遗忘,因此回顾复习,整理成博客。 一、环境变量是什么 Linux环境变量是存储系统或程序运行时配置信息的特殊变量,用于为程序提供配置参数…

16:00开始面试,16:08就出来了,问的问题有点变态。。。

从小厂出来,没想到在另一家公司又寄了。 到这家公司开始上班,加班是每天必不可少的,看在钱给的比较多的份上,就不太计较了。没想到4月一纸通知,所有人不准加班,加班费不仅没有了,薪资还要降40%…

私钥连接服务器(已经有服务器私钥

前言:假设我们已经有了服务器的私钥,我们怎么配置呢? 下面我会从vsc的配置角度来写 ✅ 步骤一:准备工作 安装 VS Code(如果还没装) 👉 https://code.visualstudio.com/ 安装插件:Re…

学员答题pk知识竞赛小程序怎么做

制作学员答题PK知识竞赛小程序,主要有以下步骤: 一、规划设计 明确需求:确定小程序的使用场景是校园知识竞赛、培训机构考核还是企业内部培训等。答题功能,规定答题的具体规则,包括题目类型(单选、多选、…

外观模式:简化复杂系统接口的设计模式

外观模式:简化复杂系统接口的设计模式 一、模式核心:为复杂子系统提供统一简单接口 当一个系统由多个复杂子系统组成时(如电商系统中的支付、物流、库存模块),客户端直接调用子系统会导致依赖关系复杂、代码难以维护…

uniapp-商城-36-shop 购物车 选好了 进行订单确认2 支付方式颜色变化和颜色滤镜filter

颜色滤镜&#xff0c;在好多网页都这样使用&#xff0c;滤掉彩色&#xff0c;显示黑白&#xff0c;这在一些关键的日子中都这样使用。 1、依然回到订单确认页面 看到支付的颜色了嘛&#xff1f; <view class"payType"><view class"box" :class&q…

Vue3 上传后的文件智能预览(实战体会)

目录 前言1. Demo12. Demo2 前言 &#x1f91f; 找工作&#xff0c;来万码优才&#xff1a;&#x1f449; #小程序://万码优才/r6rqmzDaXpYkJZF 爬虫神器&#xff0c;无代码爬取&#xff0c;就来&#xff1a;bright.cn 此处的基本知识涉及较少&#xff0c;主要以Demo的形式供大…

CCE13.【C++ Cont】练习题组13 静态链表专题

目录 1.B3630 排队顺序 题目 分析 代码 提交结果 2.B3631 单向链表 题目 分析 前置知识:map数组加快访问速度(简单的哈希表优化) 使用map数组的重要提醒 代码 提交结果 3.★P1160 队列安排 题目 分析 方法1:带头不循环双向链表的设计 方法2:带头循环的双向链表…

内联函数(c++)

预处理&#xff1a;优点&#xff1a;内嵌到目标代码&#xff0c;减少函数的调用。 缺点&#xff1a;在预处理阶段完成替换&#xff0c;避免了语义上的差错。 egg&#xff1a; #define SQR(X) ((X)*(X)) 函数&#xff1a;优点&#xff1a;完成了某一类操作的抽象&#xff0c;…

R7周:糖尿病预测模型优化探索

&#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客 &#x1f356; 原作者&#xff1a;K同学啊 一、数据预处理 1.设置GPU import torch.nn.functional as F import torch.nn as nn import torch, torchvisiondevice torch.device("cuda"…