Linux操作系统——线程概念

1.什么是线程?

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流


2.线程的理解——如何看待进程/线程

下面我们用一个故事来理解:系统当中是以内存,CPU为资源的,但是分配这些资源是以进程为单位的,社会中像房子车子土地这些都是社会上的资源,而我们现实社会中分配资源的基本单位是以谁为基本实体的呢?是以家庭为单位的。虽然现在我们并不是所有人都买得起房子车子的,但是绝大多数人还是有这些社会资源的,假设我们构建出一个理想国,一个家庭比如说5口人在一栋房子里,爷爷奶奶,爸爸妈妈,我自己,在这一栋房子里住着这么多人都有各自的任务,比如说爷爷奶奶的任务就是好好度过自己的晚年生活,保证自己身体健康就可以了。爸爸妈妈呢就是好好工作,养家,多赚钱,保证我上学没有后顾之忧将来给我娶媳妇的时候买一栋房子等等,我自己现在是学习的阶段,所以我的任务呢就是好好学习。虽然一家五口人每一个人都做着不同的工作,但我们有没有一件共同的工作呢?共同的工作就是让家庭的日子过好,而这里说到的五口人就是5个线程,而这个家庭就是一个进程。

用代码进行验证:

写这段代码之前我们先来认识一下man手册中pthread_create这个接口是用来创建线程的:

这个接口是需要我们传四个参数,第一个参数,第二个参数都是一个指针,第三个参数是叫我们传一个返回值为void * ,参数为void *的函数指针其实就是一个函数的入口,第四个参数就是给第三个参数提供需要传的参数,是一个void *类型的指针。

#include<iostream>
#include<pthread.h>
#include<unistd.h>void * ThreadRoutine(void * arg)
{const char* threadname = (const char *)arg;while(true){std::cout<<"I am a new thread: "<<threadname<<std::endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,ThreadRoutine,(void*)"thread 1");   //主线程while(true){std::cout<<"I am a main thread"<<std::endl;sleep(1);}return 0;
}

运行结果:

按照我们之前的单执行流来看待代码的话,这段代码是不可能连续执行两段死循环的,但是我们这里通过创建了一个线程让它去跑对应函数的死循环,而主线程跑main函数中的死循环使得两个死循环都在跑,然后我们检测发现只有一个进程在跑,这也就排除了多进程的清空,所以这也就验证了只有一个进程在跑这段代码,为了进一步验证,我们可以把他们的pid打出来看看:

把副线程执行死循环的打印语句改成:

std::cout<<"I am a new thread: "<<threadname<<", pid: "<<getpid()<<std::endl;

把主线程打印语句改成:

std::cout<<"I am a main thread, pid: "<<getpid()<<std::endl;

然后重新编译执行:

我们发现他们的pid是一样的,这也就说明了这两个线程是同一个进程的两个线程,所以他们两个线程获取的pid是同一个pid是合理的。

我们如何看出这两个线程的区别呢?下面我们学习一条新的指令来查看线程:

ps -aL

我们发现这两个线程(linux下是叫轻量级进程)的PID,TTY,TIME,CMD都是一样的,唯独LWP不一样:

而LWP就是轻量级进程(Light Weight Processes)的缩写。在操作系统层面上识别这两个轻量级进程是通过LWP来识别的,所以在操作系统调度的时候是看的是PID还是LWP呢?CPU调度的基本单位是线程,在linux系统上叫做轻量级进程,真实的操作系统调度的时候看的是LWP,而判断是否是主线程是通过判断PID是否等于LWP来进行判断的。

下面我们再修改一下代码:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<sys/types.h>
void * ThreadRoutine(void * arg)
{const char* threadname = (const char *)arg;while(true){std::cout<<"I am a new thread: "<<threadname<<", pid: "<<getpid()<<std::endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,ThreadRoutine,(void*)"thread 0");   sleep(3);pthread_t tid1;pthread_create(&tid1,nullptr,ThreadRoutine,(void*)"thread 1");   sleep(3);pthread_t tid2;pthread_create(&tid2,nullptr,ThreadRoutine,(void*)"thread 2");   sleep(3);pthread_t tid3;pthread_create(&tid,nullptr,ThreadRoutine,(void*)"thread 3");   sleep(3);//主线程while(true){std::cout<<"I am a main thread, pid: "<<getpid()<<std::endl;sleep(1);}return 0;
}

运行结果:

之前我们所看到的都是进程只有一个执行流的代码,但是今天是一个进程有多执行流的代码。其实线程就是CPU调度的基本单位,Linux内核服用了进程代码,用进程PCB模拟充当了线程,Linux中所有的线程都叫做轻量级进程,如果谈进程那就不能只谈执行流,还需要谈进程地址空间和页表。

下面再用一段代码来看看 现象:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<sys/types.h>int gcnt = 100;void * ThreadRoutine(void * arg)
{const char* threadname = (const char *)arg;while(true){std::cout<<"I am a new thread: "<<threadname<<", pid: "<<getpid()<<"gcnt: "<<gcnt<<"&gcnt: "<<&gcnt<<std::endl;gcnt--;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,ThreadRoutine,(void*)"thread 0");   sleep(3);while(true){std::cout<<"I am a main thread, pid: "<<getpid()<<"gcnt: "<<gcnt<<"&gcnt: "<<&gcnt<<std::endl;sleep(1);}return 0;
}

运行结果:

这段代码的运行结果说明对于全局变量,有一个线程修改了该全局变量的值,另一个线程立马就能看到,为什么呢?因为这些线程共享同一个进程地址空间,其实还有堆栈,共享区都是可以共享的。而相对于进程间通信是需要满足让不同的进程看到同一份资源的,但是让线程看到同一份资源比进程看到同一份资源更简单,当然线程通信是不安全的,但是确实更简单。

从上面的讲解我们知道了,创建进程是需要创建PCB,进程地址空间,页表的,而创建线程是需要在进程的基础上创建PCB就可以了,所以说叫做轻量级进程,但是在调度上谈进程与线程又有什么区别呢?调度的话切换的时候存在同一个进程里的线程间切换,也就是切换后下一个线程是该进程里的一个线程,这种切换像地址空间,页表这些东西都是不用切换的,只需要把进程中产生保存在寄存器中的一些临时性数据就可以了,而还有就是当前进程的线程切换后是另一个进程的主线程这种情况是需要把所有的寄存器全部切换的,将保存的上下文交给进程,这就也就是叫做我们之前理解的进程间切换。但是这并不是把线程叫做轻量级进程的主要原因,主要原因其实是CPU里面其实还有一个cache缓存,下面我们可以用一条命令来查看:

这是cpu集成的一个硬件级别的cache,比如说当前访问的是我们的第10行代码,那么有较大概率下一次访问的是第11行代码,也有较大概率访问第12行的代码,也就是cpu会有较大的概率访问正在访问的代码附近的代码,虽然有存在一些特殊情况像函数跳转,程序替换,但这些都是少量情况,我们把这种特性叫做局部性原理。我们之前说会有一个非常大的应用程序是目前加载一部分的,但是如何知道该加载的是哪一部分呢?其实这就需要用到我们的局部性原理了,就是按代码顺序加载,这给我们的预加载机制提供了理论基础,同样的cpu也有cache缓存,所以也可以先加载一部分附近的代码,这样会使cpu执行效率更高。我们一般把放到cpu缓存的数据叫做热数据,我们的线程间 切换是不用重新预加载cache里面的热数据的,而进程间 切换需要重新预加载cache中的热数据,所以这就是为什么线程间切换比进程间切换更高效的主要原因。时间片是以进程为单位进行分配的,所以进程内部的线程要对进程分配的时间片进行瓜分,因为时间片也是资源。

3.重谈一次地址空间——虚拟地址->物理地址

我们前面谈文件系统IO的时候说过,文件系统IO的基本单位大小是4KB,叫做文件块。

在操作系统层面上,物理内存与磁盘进行IO交互的时候,在硬件层面上,磁盘可以把数据导入内存,内存也可以把数据写入磁盘,其实从硬件角度就是把数据从一个设备拷贝到另一台设备,拷贝的过程其实在硬件上是支持的,至于怎么支持就跟设备自身的特征有关系,比如说磁盘内部是有盘片的,盘片是有对应的磁极的,我们可以通过磁头修改盘片特定的南北极就可以修改磁盘的01序列, 在计算机组成原理里面,有最基本的硬件电路,硬件电路里面有一个叫触发器,门电路,物理内存我们可以想象成由无数个充电的门电路或者是触发器构成,说白了物理内存就是无数个小的可以存01的高低电平的硬件电路,所以我们把数据写入物理内存本质是给物理内存进行充放电的过程,所以物理不能断电,一旦断电就会使数据丢失,所以数据从一个设备写入到另一个设备本质就是将电路信号从一个设备给另一个设备进行充电的过程,比如说内存将数据写入磁盘就是通过向物理内存的指定位置发送一个电脉冲的过程。这是最基本的,我们之前在谈冯诺依曼体系结构的时候说过,计算机里整机的效率,数据从一个设备到另一个设备,每一个设备都有各自的特性,本质就是一次充放电的过程,所以我们承认数据是可以在设备间进行移动的。

可是呢,从哪里开始读取呢?在磁盘的什么位置,加载对应的数据加载多少呢?它的大小是多少呢?它的权限是多少呢?它的类型是多少呢?它的特征属性是什么?什么时候加载?所有的这些东西都与硬件无关,这是更上层的东西,是通过文件系统来进行管理的,所以呢我们就需要有一个文件系统的东西,文件系统就会允许我们根据文件路径找到对应的文件打开,然后就可以通过文件的属性和内容通过inode和datablock把我们的属性和内容就可以加载到内存中,这就是我们文件加载或读取的过程,一个inode对应一个文件,一个文件的属性都在文件系统特定的分区的分组里面的特定的inode,所以inode也是数据,而文件内容也是数据,所以无论是属性还是内容都是数据,而物理内存和磁盘进行数据交互的时候是以4KB为基本单位的,所以说在文件系统层面上,你用户看这个文件是一个可执行程序,可是在文件系统的角度上来看其实是这个可执行程序是由多个4KB的块组成的,这种我们一般就称之为ELF数据段,也就是说这个文件的数据块每一个都是4KB,而对于我们的文件系统来说呢,它根本就不关心是文件的内容还是属性,它首先考虑的是把4KB大小的块先加载到内存当中,数据在写入修改时都是以4KB为基本单位的, 实际上文件属性和内容是分开存的,但是在用户看来文件属性和内容是一起的,所以将来我们想要读取文件的大小的时候对于操作系统来说,它根本就不关心我读到的是文件的属性还是内容,或者不直接关系,它首先要解决的是我们4KB的大小先换入内存当中,如果数据有修改就再写入对应的内容中,所以呢,计算机在设计的时候可执行程序也用4KB的大小分好了,同样的,可执行程序都按照4KB的大小分成了一块块的,所以物理内存同样的也是以4KB的大小将物理内存一块一块的分好的。所以我们把磁盘上文件以4KB为单位的块叫做页帧,而把物理内存划分成4KB的块叫做页框,把文件系统IO的基本单位4KB叫做page size.其实正是文件管理,内存管理,进程管理,背后的编译器所促成的一个文件系统IO的基本单位和内存划分的基本单位为4KB.而文件除了内容和属性,还有一个就是对应的文件缓冲区,所以文件的缓冲区的本质是本质就是把一个文件相关的一些内容或者属性放在内存不同位置的关联起来,所以文件的缓冲区就是struct file与内存中属于该文件的数据所构成的关联关系。如果我们要把比如说4GB的物理内存划分成页框可以划分成100多万个页框。所以操作系统怎么知道内存分配了多少页框,以及这些页框的使用情况?那么这些情况操作系统要不要知道呢?答案是要的,如何管理?先描述再组织。把页框描述成一个struct page里面有描述一个page的使用情况,以及page的属性等等,然后组织一个struct page pages[1048576];这么大的数组,数组下标与页框就有一个对应的映射关系,所以对内存进行管理就变成了对数组内容的增删查改。但是实际的内存管理肯定是要比这个例子更复杂的。我们知道进程的虚拟地址空间是通过页表来进行映射的,但是地址空间可是有2^32个地址,如果每一个地址都在页表上进行映射的话,关页表当中的页表项,但是如果我们的页表项要用到10个字节的话,那么物理内存都装不下页表,因为这样的页表太大了,算下来关存页表就需要40GB的空间。那么虚拟地址与物理地址之间到底是如何转化的呢?我们所知道的虚拟地址是32个比特位的。整个虚拟地址不是用一个整体来看的,而是将他拆成了三个部分,前十个比特位称之为页目录有1024个页目录中的内容存放的是对应的页表,中间十个比特位称之为页表也是1024个,而页表的内容存放的是物理内存的页框的起始地址。接下来还有12个比特位就是4096个比特位也就是4KB,这12个比特位我们可以用来当偏移量,用起始地址+偏移量就可以找到对应的物理地址了。页表其实不会被全部一次性使用完,也就是进程地址空间的4GB大小的地址并不会全都使用,而是通过需要执行所需要的指令的时候通过发生缺页中断来进行加载所需要的内容再创建对应的页表,然后把地址写入对应的页表当中去,CPU当中有寄存器保存当前进程使的页目录起始地址,eip寄存器保存的虚拟地址,而MMU完成虚拟地址到物理地址的转化,所以虚拟地址到物理地址的转化工作是在CPU内部转化的。

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

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

相关文章

openGauss学习笔记-242 openGauss性能调优-SQL调优-典型SQL调优点-SQL自诊断

文章目录 openGauss学习笔记-242 openGauss性能调优-SQL调优-典型SQL调优点-SQL自诊断242.1 SQL自诊断242.1.1 告警场景242.1.2 规格约束 openGauss学习笔记-242 openGauss性能调优-SQL调优-典型SQL调优点-SQL自诊断 SQL调优是一个不断分析与尝试的过程&#xff1a;试跑Query&…

【Qt】常用控件或属性(1)

需要云服务器等云产品来学习Linux可以移步/-->腾讯云<--/官网&#xff0c;轻量型云服务器低至112元/年&#xff0c;新用户首次下单享超低折扣。 目录 一、QWidget属性一览 二、控件button、属性enabled(可用状态) 三、属性geometry(修改位置和尺寸) 1、QRect类型的结…

微信小程序之tabBar

1、tabBar 如果小程序是一个多 tab 应用&#xff08;客户端窗口的底部或顶部有 tab 栏可以切换页面&#xff09;&#xff0c;可以通过 tabBar 配置项指定 tab 栏的表现&#xff0c;以及 tab 切换时显示的对应页面。 属性类型必填默认值描述colorHexColor是tab 上的文字默认颜色…

Leetcode 3.14

Leetcode hot100 二叉树1.二叉树的层序遍历2.验证二叉搜索树3.二叉树的右视图 二叉树 1.二叉树的层序遍历 二叉树的层序遍历 二叉树的层序遍历可以用先进先出的队列来实现。 将每一层的所有node都添加到队列中&#xff0c;记录下当前队列的长度&#xff0c;即该层的元素数量&…

『 Linux 』进程替换( Process replacement ) 及 简单Shell的实现(万字)

文章目录 &#x1f984; 进程替换&#x1f9a9; execl()函数&#x1f9a9; execlp()函数&#x1f9a9; execle()函数&#x1f9a9; execv()函数&#x1f9a9; execvp()函数&#x1f9a9; execvpe()函数&#x1f9a9; execve()函数 &#x1f984; 简单Shell命令行解释器的实现&a…

【漏洞复现】ChatGPT-Next-Web SSRF漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…

Python-sklearn-diabetes项目实战

目录 1 下载数据集和预处理 1.1 加载/下载数据集 1.2 数据可视化 1.3 数据清洗 1.4 特征工程 1.5 构建特征集和标签集 1.6 拆分训练集和测试集 2 训练模型 2.1 选择算法和确定模型 2.2 训练拟合模型 3 评估并优化模型性能 本文以糖尿病数据集diabetes为基础进行线性…

【Node.js从基础到高级运用】十一、构建RESTful API

在本篇博客中&#xff0c;我们将综合之前讨论的内容&#xff0c;深入探索如何使用Node.js构建一个RESTful API。我们将重点讨论设计合理的API端点&#xff0c;展示如何通过代码实现这些端点&#xff0c;并指导如何使用Postman测试我们的API&#xff0c;确保其按预期工作。 前提…

完美解决 RabbitMQ可视化界面Overview不显示折线图和队列不显示Messages

问题场景&#xff1a; 今天使用docker部署了一个RabbitMQ&#xff0c;浏览器打开15672可视化页面发送消息后不显示Overview中的折线图&#xff0c;还有队列中的Messages&#xff0c;因为我要看队列中的消息数量。 解决方案&#xff1a; 进入容器内部 docker exec -it 容器id…

Spring Cloud Gateway如何实现熔断

Spring Cloud Gateway熔断集成 熔断应用&#xff1a; 金融市场中的熔断机制&#xff1a;在金融交易系统中&#xff0c;熔断机制&#xff08;Circuit Breaker&#xff09;是一种市场保护措施&#xff0c;旨在预防市场剧烈波动时可能导致的系统性风险。当某个基准指数&#xff08…

MySQL数据库(5)——逻辑处理

逻辑处理 在MySQL中&#xff0c;可以使用逻辑运算符和条件语句来进行逻辑处理。 1.逻辑运算符 and 与or 或not 非 这里用法与Java中的用法相同&#xff0c;只是语法发生了部分变化。 2.case——when逻辑处理 语法&#xff1a; case when 条件 then 结果1 else 结果2 end 注…

基于springboot实现驾校信息管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现驾校信息管理系统演示 摘要 随着人们生活水平的不断提高&#xff0c;出行方式多样化&#xff0c;也以私家车为主&#xff0c;那么既然私家车的需求不断增长&#xff0c;那么基于驾校的考核管理也就不断增强&#xff0c;那么业务系统也就慢慢的随之加大。信息…

15. jwt认证中间件

在上一篇登录功能的实现中&#xff0c;我们使用了jwt作为鉴权组件&#xff0c;其中登录后会颁发token。前端在访问后续请求时&#xff0c;可以带上这个token。对于一些需要权限校验的请求&#xff0c;我们就需要验证这个token&#xff0c;从token中获取到用户id&#xff08;有了…

MQTT.fx实现(MQTT Client)连接物联网平台(ThingsCloud )

目录 概述 1 ThingsCloud平台介绍 2 ThingsCloud创建项目 2.1 创建项目 2.2 创建数据类型 2.3 创建设备类型 3 发布和订阅主题 3.1 发布Topic 3.2 订阅主题Topic 4 使用MQTT.fx 测试ThingsClond 4.1 使用MQTT.fx连接ThingsClond 4.2 MQTT.fx 订阅ThingsCloud Messag…

jenkins+maven+gitlab自动化构建打包、部署

Jenkins自动化部署实现原理 环境准备 1、jenkins已经安装好 docker安装jenkins 2、gitlab已经安装好 docker安装gitlab 一、Jenkins系统配置 1.Global Tool Configuration 任务构建所用到的编译环境等配置&#xff0c;配置参考&#xff1a; jdk配置&#xff08;jenkins自带…

GCNv2_SLAM-CPU详细安装教程(ubuntu18.04)

GCNv2_SLAM-CPU详细安装教程-ubuntu18.04 前言一、安装第三方库1.安装Pangolin2.安装OpenCV3.安装Eigen4.安装Pytorch(c) 二、安装以及运行GCNv2_SLAM1.安装编译GCNv2_SLAM2.单目模式运行演示案例 总结 前言 paper:https://arxiv.org/pdf/1902.11046.pdf githup::https://gith…

linux 安装gradle7.4.2环境

1.下载gradle7.4.2工程 百度网盘 请输入提取码百度网盘为您提供文件的网络备份、同步和分享服务。空间大、速度快、安全稳固&#xff0c;支持教育网加速&#xff0c;支持手机端。注册使用百度网盘即可享受免费存储空间https://pan.baidu.com/s/1hoNEFkBJPHAgs9ITAEh3Zg?pwdGJ…

DDos攻击如何被高防服务器有效防范?

德迅云安全-领先云安全服务与解决方案提供商 什么是DDos攻击&#xff1f; DDos攻击是一种网络攻击手段&#xff0c;旨在通过使目标系统的服务不可用或中断&#xff0c;导致无法正常使用网络服务。DDos攻击可以采取多种方式实施&#xff0c;包括洪水攻击、压力测试、UDP Flood…

WPF —— TabControl、StackPanel 控件详解

1 TabControl简介 表示包含多个项的控件&#xff0c;这些项共享屏幕上的同一空间。 TabControl有助于最大程度地减少屏幕空间使用量&#xff0c;同时允许应用程序公开大量数据。 TabControl包含共享同一屏幕空间的多个 TabItem 对象。一次只能看到 TabControl 中的一个 Ta…

java的23种设计模式02-创建型模式02-抽象工厂方法

一、抽象工厂方法 1-1、抽象工厂方法的定义 抽象工厂模式是一个比较复杂的创建型模式。 抽象工厂模式和工厂方法不太一样&#xff0c;它要解决的问题比较复杂&#xff0c;不但工厂是抽象的&#xff0c;产品是抽象的&#xff0c;而且&#xff1a;有多个产品需要创建&#xff…