Linux--线程(概念篇)

目录

1.背景知识

再谈地址空间:

关于页表(32bit机器上)

 2.线程的概念和Linux中线程的实现

概念部分:

代码部分:

问题:

3.关于线程的有点与缺点 

4.进程VS线程  


1.背景知识

再谈地址空间:

        我们都知道系统和磁盘文件进行IO的基本单位是内存块4KB--8个扇区。我们以4GB大小的物理内存为例,物理内存被分为一个一个的页框,一个页框的大小也就是4KB,那么我们也就清楚了,磁盘加载到物理内存,操作系统会从磁盘中读取该页面并将其加载到物理内存中的一个页框/页帧中。

        当我们谈及操作系统对内存的管理工作,基本单位也是4KB! 

        现在有一个问题:在父子进程进行共享内存的全局变量int只占四个字节,我对他写入时要发生写时拷贝,写时拷贝的本质就让操作系统重新申请内存,那么拷贝的时候是拷贝四个字节还是4kb呢?

        对于全局变量int的写入操作,通常不会触发写时拷贝。全局变量是在进程的地址空间中分配的,每个进程都有自己的全局变量副本(除非它们通过某种形式的共享内存机制显式地共享)。当你修改一个全局int变量时,你只是在当前进程的地址空间中修改了该变量的4个字节。

        如果全局变量是通过某种形式的共享内存在不同的进程之间共享的,并且你在这些进程之一中修改了该变量,这时一般会触发写时拷贝,写时拷贝也不会仅仅拷贝4个字节;相反,它会拷贝包含该变量的整个页框(即4KB)。如果操作系统在每次修改共享内存中的变量时都只拷贝变量的实际大小,那么这将大大增加管理的复杂性,并可能导致内存碎片化。通过以页面为单位进行拷贝,操作系统可以简化内存管理,减少内存碎片,并提高内存访问的效率。

        那么操作系统是如何对物理内存做管理的呢?

      首先物理内存是被划分为一个一个的页框的,若物理内存的大小为4GB,那么页框的数量就有1048576个,那么操作系统就要知道这些页框的使用状态,那么操作系统是如何管理这些页框的呢? 操作系统由对应的结构体struct page ,其中int flag变量就是管理页框是否被占有,是否有脏页,是否被锁定的,还会包含mode(权限),等等。 struct page memory[1048576]把内存管理起来,用下标转化为每一个页框的起始地址。

关于页表(32bit机器上)

                我们都知道虚拟地址是32个比特位组成的,一共有2^32个。

        虚拟地址是如何转化为物理地址的呢?

        我们都知道虚拟地址转化为物理地址都是要通过页表映射,关键就在于页表。页表并不是简单的一一映射,他是有多级结构的,以32bit机器为例:    

在32位系统中,虚拟地址的32个比特位通常按照以下方式划分(以多级页表为例):

  1. 页目录索引:高位的比特位用于索引页目录。页目录是一个包含多个页表项的数组,每个页表项指向一个页表。页目录索引的位数决定了页目录中页表项的数量,进而影响页目录的大小。

  2. 页表索引:紧接着页目录索引之后的比特位用于索引页表。页表也是一个包含多个页表项的数组,每个页表项包含物理页帧的起始地址和其他信息(如访问权限)。页表索引的位数决定了页表中页表项的数量,进而影响页表的大小。

  3. 页内偏移:最低位的比特位用于在物理页帧内定位数据。页内偏移的位数决定了页帧的大小,通常是固定的(如4KB)。

具体划分示例

以常见的32位系统为例,虚拟地址的32个比特位可能被划分为10-10-12的形式:

  • 高10位:作为页目录索引,可以索引到最多1024(2^10)个页表。
  • 中间10位:作为页表索引,每个页表可以包含最多1024(2^10)个页表项。
  • 低12位:作为页内偏移,用于在4KB(2^12字节)的页帧内定位数据。一个页帧的大小刚好是4KB,也就是说,页内偏移量可以定位到每一个字节。
  • 那么我们也就知道了,前20位的作用就是定位到页框号,本质就是搜索页框,后12那就是用来定位页框内的如何一个字节。这个方案就叫二级页表。这大大的节省了空间(1024个页表*2KB=2MB+4kb页目录,这是在拉满的情况下),在这种方式下,只要知道取的数据是什么类型,就知道要取几个字节,就能获取数据了。

 CPU想要通过页表获取物理地址,首先就是要找到页表,那么页表在哪里呢?

        CR3:控制寄存器3,也被称为PDBR(页目录基址寄存器),用于存储页目录表的物理地址。通过改变CR3寄存器的值,可以实现不同虚拟地址空间之间的切换。

        MMU接收到CPU发出的虚拟地址后,会根据当前CR3寄存器中存储的页目录表物理地址,以及虚拟地址的结构(如页目录索引、页表索引、页内偏移等),在页目录表和页表中查找对应的物理地址。最后,从CPU中出来的直接就是虚拟地址。

                        


 2.线程的概念和Linux中线程的实现

概念部分:

线程:在进程内部运行,是cpu调度的基本单位。

初步理解:在下面,一个一个的tesk_struct就是一个一个的执行流,地址空间的正文代码也会被分为4部分,让每一个执行流去执行,这一个一个的执行流就是Linux中的线程,这是我们对线程的初步理解,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

在学习进程的时候我们得出结论:进程=内核数据结构+进程的代码和数据。

现在我们从内核观点给出进程的定义:进程是承担分配系统资源的基本实体!

对比以前对进程的理解区别在于:内部只有一个执行流的进程。 

OS关于线程的设计

        在windows系统下,线程是真实存在的,有自己的控制结构体与调度算法;

        从内核的角度来看,Linux并没有线程这个概念。Linux的线程通常被当作一种特殊的进程(是进程模拟的)来实现。每个线程都拥有自己独立的task_struct内核数据结构对象,但在进程内部,多个线程共享进程的地址空间和其他资源。

       

         对于CPU来说,调度一个task_struct<=进程,因为task_struct可能只是一个进程的一个执行流。那么CPU要不要区分task_struct是进程还是线程?

        当然不必区分,对于CPU来说都叫做执行流,所以之前与进程有关的知识,在Linux下仍然适用,因为线程就是一个特殊的进程。(CPU看到的执行流<=进程。因此我们称Linux中的执行流:轻量级进程!!!


代码部分:

先见一见:

引入函数pthread_create,,用于在程序中创建一个新的线程

参数说明:

  • thread:指向 pthread_t 类型的指针,用于存储新创建的线程的标识符。成功调用后,这个标识符可以用来引用该线程。
  • attr:指向 pthread_attr_t 类型的指针,用于设置线程的属性,如线程栈的大小、调度策略等。如果传递 NULL,则使用默认属性。
  • start_routine:线程将要执行的函数的指针。这个函数应该接受一个 void* 类型的参数,并返回一个 void* 类型的值。这个函数是线程开始执行时调用的函数。
  • arg:传递给 start_routine 函数的参数。这个参数的类型是 void*,这意味着你可以传递任何类型的指针。

主线程和新创建的线程会并行执行,直到新线程完成其任务。

eg:两个执行流同时跑死循环

在进行线程的编译时,要引入第三方库:pthread:它提供了一套创建和管理线程的API。这些API使得在多种UNIX系统上编写多线程程序成为可能,同时也增强了程序的可移植性。

编译时要带-lpthread链接pthread库

test1:test.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -rf test1

代码:

#include <iostream>
#include <unistd.h>//新进程
void *threadStart(void *args)
{while (true){sleep(1);std::cout << "new thread running..." <<std::endl;}
}int main()
{pthread_t tid1;pthread_create(&tid1, nullptr, threadStart, (void *)"thread-new");//主线程while(true){sleep(1);std::cout << "main thread running..." <<std::endl;}return 0;
}

同时执行两个死循环,这就是一个多线程的代码。

这时候你查询系统中的进程时,发现只有一个进程

更改代码后,让它们打出各自的pid,果然都一样:

原因是:这两个线程属于一个进程内部。

        但也是可以通过命令看到有几个线程的:ps -aL,我们可以看到LWP(Lightweight Process)轻量级进程,OS进行调度的时候看的就是LWP,而不是PID,LWP才是标识一个 执行流的概念,LWP和PID相等的执行流,我们称之为主线程(特殊情况:多进程,单进程调度时看OS根据PID来区分,这不矛盾,因为在这两种情况下PID==LWP

        每个线程都有自己要执行的代码,每行代码都有自己的地址,在逻辑上只要每个线程拿到自己代码所对应的那部分页表,就能找到自己执行代码的地址了,就能执行代码了。


问题:

        1.已经有多进程了,为什么要有多线程呢?

        创建: 首先进程创建的成本是非常高的(进程是系统资源分配的基本单位,每个进程都拥有独立的地址空间、内存、文件描述符等资源。)而创建线程:1.创建PCB 2.将进程已有的资源获取就好了。

        运行:线程调度成本低

        删除一个线程的成本也是低的

       2. 线程这么好,为什么要有进程呢?

        由于线程共享进程的内存空间,因此一个线程中的错误可能会影响到进程中的其他线程。例如,如果一个线程发生段错误(如访问了非法地址),则可能导致整个进程崩溃,进而影响到该进程内的所有线程。相比之下,进程间的独立性使得一个进程的崩溃不会影响到其他进程。(健壮性降低,当然还有其它方面,进程和线程都有自己的不可取代性)。

       3.线程调度的成本为什么低?

        CPU为了加速访存会存在一个cache的硬件,它会遵循局部性原理,将执行代码的前几行和后几行全都加载到cache当中,这一部分我们称为进程执行的热数据。当CPU执行到某行代码的时候,如果这部分缓存命中了,则直接从cache中读取,如果没命中,再从内存中缓存,重新置换到cache当中。

        这意味着,如果是A,B进程间要进行切换,除了pcb,地址空间,页表要切,A和B要执行的任务肯定是不一样的,进程Acache缓存的热数据,进程B用不上,这意味着进程B要重新cache,这就慢了。但线程进行切换的时候,由于线程共享进程的地址空间和资源,因此缓存中的内容仍然有效,无需进行替换。这减少了缓存失效的次数和缓存加载的时间,从而降低了调度的成本。(主要矛盾)  


3.关于线程的有点与缺点 

优点:

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

缺点:

  • 性能损失

        一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

  • 健壮性降低

        编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

eg:我们写了一段代码, 我们发现创建出3个线程,加上一个主线程,只要有一个线程出问题了,其它的线程就都受影响终止了(一个线程出问题,OS就是识别到整个进程出问题,OS就会给进程发信号,每个线程都要处理)。

#include <iostream>
#include <unistd.h>
#include <ctime>// 新线程void *threadStart(void *args)
{while (true){int x = rand() % 5;std::cout << "new thread running..." << ", pid: " << getpid()<<":"<< x <<std::endl;sleep(1);if(x == 0){int *p = nullptr;*p = 100; // 野指针}}
}int main()
{srand(time(nullptr));pthread_t tid1;pthread_create(&tid1, nullptr, threadStart, (void *)"thread-new");pthread_t tid2;pthread_create(&tid2, nullptr, threadStart, (void *)"thread-new");pthread_t tid3;pthread_create(&tid3, nullptr, threadStart, (void *)"thread-new");// 主线程while(true){sleep(1);std::cout << "main thread running..." <<",pid"<<getpid()<<std::endl;}return 0;
}

  • 缺乏访问控制

        进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

eg:我们发现只要主线程更改了全局变量gvall的值,其它线程都是会受影响的,因为线程大部分的资源都是共享的

#include <iostream>
#include <unistd.h>
#include <ctime>int gval = 100;// 新线程
void *threadStart(void *args)
{while (true){sleep(1);std::cout << "new thread running..." << ", pid: " << getpid()<< ", gval: " << gval << ", &gval: " << &gval << std::endl;}
}int main()
{srand(time(nullptr));pthread_t tid1;pthread_create(&tid1, nullptr, threadStart, (void *)"thread-new");pthread_t tid2;pthread_create(&tid2, nullptr, threadStart, (void *)"thread-new");pthread_t tid3;pthread_create(&tid3, nullptr, threadStart, (void *)"thread-new");// 主线程while (true){std::cout << "main thread running..." << ", pid: " << getpid()<< ", gval: " << gval << ", &gval: " << &gval << std::endl;gval++; // 修改!sleep(1);}return 0;
}

  • 编程难度提高

        编写与调试一个多线程程序比单线程程序困难得多


4.进程VS线程  

        进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程 中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

进程和线程的关系如下图:

进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:

  • 线程ID
  • 一组寄存器(与硬件上下文数据有关--线程是在动态运行的
  • 栈(线程在运行的时候,本质是在运行一个函数,会形成各种临时变量,临时变量会被每个线程保存在自己的栈区)
  • errno
  • 信号屏蔽字
  • 调度优先级
     

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

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

相关文章

Linux中的粘滞位及mysql日期函数

只要用户具有目录的写权限, 用户就可以删除目录中的文件, 而不论这个用户是否有这个文件的写 权限. 为了解决这个不科学的问题, Linux引入了粘滞位的概念. 粘滞位 当一个目录被设置为"粘滞位"(用chmod t),则该目录下的文件只能由 一、超级管理员删除 二、该目录…

FreeRTOS 列表和列表项

这里推荐看完韦东山的C语言本质和韦东山的rtos快速入门视频 在 FreeRTOS 的源码中大量地使用了列表和列表项&#xff0c;因此想要深入学习 FreeRTOS&#xff0c;列表和 列表项是必备的基础知识。这里所说的列表和列表项&#xff0c;是 FreeRTOS 源码中 List 和 List Item 的 直…

高创新 | CEEMDAN-VMD-GRU-Attention双重分解+门控循环单元+注意力机制多元时间序列预测

目录 效果一览基本介绍模型设计程序设计参考资料 效果一览 基本介绍 高创新 | CEEMDAN-VMD-GRU-Attention双重分解门控循环单元注意力机制多元时间序列预测 本文提出一种基于CEEMDAN 的二次分解方法&#xff0c;通过样本熵重构CEEMDAN 分解后的序列&#xff0c;复杂序列通过VMD…

Redhat 安装 docker 网络连接超时问题

目录 添加阿里云的Docker CE仓库 更新YUM缓存 安装 Docker Engine 启动并设置Docker自启动 验证 Docker 安装 [userlocalhost ~]$ sudo yum-config-manager --add-repohttps://download.docker.com/linux/centos/docker-ce.repo 正在更新 Subscription Management 软件仓库…

Linux安装Jmeter及简单使用教程

Linux安装Jmeter 首先需要java环境 java --version官网 下载二进制包 #创建文件夹 sudo mkdir /usr/local/jmeter #解压 sudo tar zxvf apache-jmeter-5.6.3.tgz -C /usr/local/jmeter编辑配置文件 sudo vim /etc/profile&#xff0c;添加以下内容 export JMETER_HOME/usr/l…

Linux环境部署Python Web服务

“姑娘&#xff0c;再见面就要靠运气了&#xff0c;可别装作不认识&#xff0c;那句“好久不见”可干万别打颤…” 将使用 Python 编写的后端 API 部署到 Linux 环境中&#xff0c;可以按照以下详细步骤操作。本文将涵盖环境准备、API 编写、使用 Gunicorn 作为 WSGI 服务器、配…

1-3分钟爆款视频素材在哪找啊?这9个热门爆款素材网站分享给你

在如今快节奏的时代&#xff0c;短视频已成为吸引观众注意力的黄金手段。然而&#xff0c;要制作出1-3分钟的爆款视频&#xff0c;除了创意和剪辑技巧外&#xff0c;选择合适的素材至关重要。那么&#xff0c;哪里可以找到那些能让你的视频脱颖而出的爆款素材呢&#xff1f;不用…

鸿蒙开发:Universal Keystore Kit(密钥管理服务)【加解密(ArkTS)】

加解密(ArkTS) 以AES 128密钥为例&#xff0c;完成加解密。具体的场景介绍及支持的算法规格。 开发步骤 生成密钥 指定密钥别名。初始化密钥属性集。调用[generateKeyItem]生成密钥&#xff0c;具体请参考[密钥生成]。开发前请熟悉鸿蒙开发指导文档&#xff1a;gitee.com/l…

yolov8-seg分割模型TensorRt部署,去掉torch

已完成的yolov8-seg分割模型TensorRt部署 准备下载yolov8-seg模型转化为onnx和trt推理写好的推理接口 准备 https://github.com/songjiahao-wq/yolov8_seg_trtinference.git下载代码 安装TensorRt8.6版本&#xff0c;以及pip install -r requirements.txt 下载yolov8-seg模型…

【web APIs】快速上手Day05(Bom操作)

目录 Web APIs - 第5天笔记js组成window对象BOM定时器-延迟函数案例-5秒钟之后消失的广告 JS执行机制location对象案例-5秒钟之后跳转的页面 navigator对象histroy对象 本地存储&#xff08;今日重点&#xff09;localStorage&#xff08;重点&#xff09;sessionStorage&#…

三、mysql-万字长文读懂mysql

mysql 三、 Mysql3.1 基础3.1.1 mysql执行流程-组成架构3.2 索引3.2.1 索引底层的数据结构与算法分类在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引B+树结构3.2.2 为什么 MySQL InnoDB 选择 B+tree 作为索引的数据结构3.2.2.1. 从磁盘角度出发3.2.2.2. 数据…

深度解析移动硬盘“函数不正确”错误及高效恢复策略

在数据密集型的社会中&#xff0c;移动硬盘作为移动存储的重要载体&#xff0c;承载着无数用户的个人信息、工作资料及珍贵回忆。然而&#xff0c;当遭遇“函数不正确”的错误时&#xff0c;这些宝贵的数据仿佛被一层无形的屏障所阻隔&#xff0c;让人束手无策。本文将深入探讨…

如何选择高性价比的土壤检测仪器?

在现代农业与环保领域&#xff0c;土壤检测仪器的选择显得尤为关键。它不仅关系到土壤养分管理、作物健康生长&#xff0c;还涉及到环境保护和可持续发展。那么&#xff0c;面对市场上琳琅满目的土壤检测仪器&#xff0c;我们该如何选择一款实用的设备呢&#xff1f; 首先&…

(1)滑动窗口算法介绍与练习:长度最小的子数组

滑动窗口算法介绍 所谓滑动窗口&#xff0c;即为同向双指针移动过程中形成的间隔区域&#xff0c;并且这两个指针在移动的过程中不会回退 对于滑动窗口的题目可以抽象为三个步骤&#xff1a; 定义窗口两端指针left和right进入窗口判断离开窗口循环2、3和4步 滑动窗口练习 长度最…

短视频电商源码的优势及软件架构解析

短视频电商源码是目前电商行业中非常火热的一个新兴领域&#xff0c;它通过短视频内容和电商商品的结合&#xff0c;为用户提供了一种新的购物体验。下面将介绍短视频电商源码的优势以及软件架构。 首先&#xff0c;短视频电商源码具有以下几个优势&#xff1a; 1、创新的购物体…

惠海 H6118 DCDC降压恒流芯片IC 30V36v40V降12V 9V LED景观灯舞台灯方案

H6118是一款连续电感电流导通模式的降压型LED恒流驱动器&#xff0c;用于驱动一个或多个LED 灯串。H6118工作电压从4V到30V&#xff0c;提供可调的输出电流&#xff0c;最大输出电流可达到1.2A。 H6118内置功率开关管&#xff0c;采用高端电流检测电路&#xff0c;支持PWM模式…

云联壹云 FinOps:赋能某车企公有云成本管理与精细化运营

背景 某车企&#xff0c;世界 500 强企业&#xff0c;使用了大量的公有云资源&#xff0c;分布于多家公有云&#xff0c;月消费在千万级别。 业务线多且分散&#xff0c;相关的云消耗由一个核心团队进行管理&#xff0c;本次案例的内容将围绕这些云成本的管理展开的。 需求 …

用例导图CMind

突然有一些觉悟&#xff0c;程序猿不能只会吭哧吭哧的低头做事&#xff0c;应该学会怎么去展示自己&#xff0c;怎么去宣传自己&#xff0c;怎么把自己想做的事表述清楚。 于是&#xff0c;这两天一直在整理自己的作品&#xff0c;也为接下来的找工作多做点准备。接下来…

超详细kkFileView打包部署Windows或Liunx

目录 前言 下载源码编辑打包 Windows下的部署 Liunx下的部署 前言 本文章主要以下载源码 自己编译打包的方式进行部署。 因为4.0.0之后官方不在初始jar包,所以自己拉代码吧,别偷懒,顺便看看代码怎么写的。 码云: kkFileView 下载源代码为4.4.0-beta版本,亲测可用 下载源…

C++的map / multimap容器

一、介绍 在C的map / multimap容器中&#xff0c;所有的元素均是pair类型&#xff08;有关pair类型可以参考我之前写的 《C的set / multiset容器》的3.2中有介绍到&#xff09;。 每对pair的第一个元素被称为关键字key&#xff0c;第二个元素被称为值value。因此&#xff0c;ma…