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.cc
	g++ -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.mfbz.cn/a/784265.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【TB作品】51单片机 Proteus仿真00016 乒乓球游戏机

课题任务 本课题任务 (联机乒乓球游戏)如下图所示: 同步显示 oo 8个LED ooooo oo ooooo 8个LED 单片机 单片机 按键 主机 从机 按键 设计题目:两机联机乒乓球游戏 图1课题任务示意图 具体说明: 共有两个单片机,每个单片机接8个LED和1 个按键,两个单片机使用串口连接。 (2)单片机…

视频号矩阵管理系统:短视频内容营销的智能助手

随着短视频行业的蓬勃发展&#xff0c;视频号矩阵管理系统应运而生&#xff0c;为内容创作者和品牌提供了一站式的短视频管理和营销解决方案。本文将深入探讨视频号矩阵管理系统的核心功能&#xff0c;以及它如何助力用户在短视频营销领域取得成功。 视频号矩阵管理系统概述 …

C++语言相关的常见面试题目(一)

1. const关键字的作用 答&#xff1a; 省流&#xff1a;&#xff08;1&#xff09;定义变量&#xff0c;主要为了防止修改 (2) 修饰函数参数&#xff1a;防止在函数内被改变 &#xff08;3&#xff09;修饰函数的返回值 &#xff08;4&#xff09;修饰类中的成员函数 2. Sta…

怎样卸载电脑上自带的游戏?

卸载电脑上自带的游戏通常是一个简单的过程&#xff0c;以下是几种常见的方法&#xff0c;您可以根据自己的操作系统版本选择相应的步骤进行操作&#xff1a; 方法一&#xff1a;通过“设置”应用卸载&#xff08;适用于Windows 10和Windows 11&#xff09; 1. 点击开始菜单&…

fastjson-1.2.24漏洞复现

文章目录 0x01 前言0x02 环境0x03漏洞复现环境准备 0x04 漏洞分析利用链源码分析 0x05 总结0x06 可能遇到的坑 0x01 前言 影响版本 fastjson < 1.2.24 本文出于学习fastjson漏洞的目的&#xff0c;为了能更好的复现漏洞&#xff0c;需要有以下前置知识。 springbootfastj…

nginx配置反向代理-CSDN

客户需求 1、实现通过域名访问税金的发票服务&#xff08;路径格式要求&#xff1a;https://www.xxx.com&#xff09; nginx的部署 前提 1、客户在局域网内已实现通过https://ip:port/stms访问税金平台 2、客户已获取https的SSL证书 3、客户申请的外网ip和域名已绑定 部署…

LeetCode——第 405 场周赛

题目 找出加密后的字符串 给你一个字符串 s 和一个整数 k。请你使用以下算法加密字符串&#xff1a; 对于字符串 s 中的每个字符 c&#xff0c;用字符串中 c 后面的第 k 个字符替换 c&#xff08;以循环方式&#xff09;。 返回加密后的字符串。 示例 1&#xff1a; 输入&…

Visual Studio Code 教程 VsCode安装Live Server以服务形式打开html

搜索Live Server 插件,然后安装 选一个html文件&#xff0c;右键点击 Open with live server,然后就自动弹出来了

最优化方法 运筹学【】

1.无约束 常用公式 线搜索准则&#xff1a;求步长 精确线搜索&#xff08;argmin&#xff09; 最速下降&#xff1a;sd&#xff1a;线性收敛 2.算法 SD dk&#xff1a;付梯度-g newton dk&#xff1a;Gkd-g 二阶收敛&#xff0c;步长为1 阻尼牛顿&#xff1a;步长用先搜…

数据分析与挖掘实战案例-电商产品评论数据情感分析

数据分析与挖掘实战案例-电商产品评论数据情感分析 文章目录 数据分析与挖掘实战案例-电商产品评论数据情感分析1. 背景与挖掘目标2. 分析方法与过程2.1 评论预处理1. 评论去重2. 数据清洗 2.2 评论分词1. 分词、词性标注、去除停用词2. 提取含名词的评论3. 绘制词云查看分词效…

Linux dig命令常见用法

Linux dig命令常见用法 一、dig安装二、dig用法 DIG命令(Domain Information Groper命令)是常用的域名查询工具&#xff0c;通过此命令&#xff0c;你可以实现域名查询和域名问题的定位&#xff0c;对于网络管理员和在域名系统(DNS)领域工作的小伙伴来说&#xff0c;它是一个非…

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

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

汇川CodeSysPLC教程 Modbus变量编址

线圈&#xff1a;位变量&#xff0c;只有两种状态0和1。汇川PLC中包含Q区及SM区等变量。 寄存器&#xff1a;16位&#xff08;字&#xff09;变量&#xff0c;本PLC中包含M区及SD区等变量 说明&#xff1a; 汇川HMI的专用协议使用不同功能码&#xff1a;在访问SM时&#xff0c…

基于Java+SpringMvc+Vue技术的实验室管理系统设计与实现(6000字以上论文参考)

博主介绍&#xff1a;硕士研究生&#xff0c;专注于信息化技术领域开发与管理&#xff0c;会使用java、标准c/c等开发语言&#xff0c;以及毕业项目实战✌ 从事基于java BS架构、CS架构、c/c 编程工作近16年&#xff0c;拥有近12年的管理工作经验&#xff0c;拥有较丰富的技术架…

使用AI学习英语

使用AI学英语可以通过与智能AI对话、模拟对话场景、提供即时反馈和个性化学习计划等方式提高学习效率和效果。然而&#xff0c;AI技术也存在局限性&#xff0c;如缺乏情感交流和真实语境&#xff0c;需要与真人教师结合使用。 AI学英语的基本原理和应用 AI的基本原理 AI&…

人工智能概论 | 基于A*算法的8数码问题求解

大学四年的全部课程和考试都已经结束啦&#xff01; 最近闲来无事&#xff0c;随便发发自己的实验报告&#xff0c;供后面的学弟学妹们参考~ 目录 实验1 基于A*算法的8数码问题求解 1.1 程序总体流程 1.2 关键代码展示 1.3 输出结果展示及分析 1.3.1 总步数展示 1.…

Python编程学习笔记(1)--- 变量和简单数据类型

1、变量 在学习编程语言之前&#xff0c;所接触的第一个程序&#xff0c;绝大多数都是&#xff1a; print("Hello world!") 接下来尝试使用一个变量。在代码中的开头添加一行代码&#xff0c;并对第二行代码进行修改&#xff0c;如下&#xff1a; message "…

云计算【第一阶段(27)】DHCP原理与配置以及FTP的介绍

一、DHCP工作原理 1.1、DHCP概念 动态主机配置协议 DHCP&#xff08;Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议&#xff0c;该协议允许服务器向客户端动态分配 IP 地址和配置信息。 DHCP协议支持C/S&#xff08;客户端/服务器&#xff09;结构&…

c++之命名空间详解(namespace)

引例 在学习之前我们首先了来看这样一个情形: 在c语言下&#xff0c;我们写了两个头文件&#xff1a;链表和顺序表的。我们会定义一个type(typedef int type)方便改变数据类型&#xff08;比如将int改成char&#xff09;&#xff0c;来做到整体代换。 但是我们两个头文件里面…

C# 实现基于exe内嵌HTTPS监听服务、从HTTP升级到HTTPS 后端windows服务

由于客户需要把原有HTTP后端服务升级为支持https的服务&#xff0c;因为原有的HTTP服务是一个基于WINDOWS服务内嵌HTTP监听服务实现的&#xff0c;并不支持https, 也不像其他IIS中部署的WebAPI服务那样直接加载HTTPS证书&#xff0c;所以这里需要修改原服务支持https和服务器环…