Linux--线程(概念篇)

news2024/9/22 17:25:51

目录

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.coloradmin.cn/o/1907837.html

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

相关文章

申请乙级测绘资质最新标准

截止到目前为止&#xff0c;测绘资质申请条件还是按照自然资源部于2021年发布的《自然资源部办公厅关于印发测绘资质管理办法和测绘资质分类分级标准的通知》&#xff08;自然资办发[2021]43号&#xff09;&#xff0c;具体内容如下&#xff0c;近期想申请测绘资质的企业可以参…

泛微E9开发 根据条件显示/隐藏明细行

根据条件显示/隐藏明细行 1、需求说明2、实现方法3、扩展知识点控制明细数据行的显示及隐藏格式参数说明演示 1、需求说明 主表字段“全部显示/隐藏”&#xff08;下拉框&#xff0c;值&#xff1a;0 全部显示、1 全部隐藏&#xff09;&#xff0c;用来控制所有明细行的显示、隐…

C++基础(十二):string类

这一篇博客&#xff0c;我们正式进入STL中的容器的字符串类的学习&#xff0c;C标准模板库&#xff08;STL&#xff09;中的std::string类是一个用于表示和操作字符串的类。它封装了动态分配的字符数组&#xff0c;提供了丰富的成员函数来进行字符串的操作&#xff0c;例如拼接…

019-GeoGebra中级篇-GeoGebra的坐标系

GeoGebra作为一款强大的数学软件&#xff0c;支持多种坐标系的使用&#xff0c;包括但不限于&#xff1a;笛卡尔坐标系&#xff08;Cartesian Coordinate System&#xff09;、极坐标系&#xff08;Polar Coordinate System&#xff09;、参数坐标系&#xff08;Parametric Coo…

国内教育科技公司自研大语言模型

好未来的数学大模型九章大模型&#xff08;MathGPT&#xff09; 2023年8月下旬&#xff0c;在好未来20周年直播活动中&#xff0c;好未来公司CTO田密宣布好未来自研的数学领域千亿级大模型MathGPT正式上线并开启公测。根据九章大模型的官网介绍&#xff0c;九章大模型&#xff…

如何使用allure生成测试报告

第一步下载安装JDK1.8&#xff0c;参考链接JDK1.8下载、安装和环境配置教程-CSDN博客 第二步配置allure环境&#xff0c;参考链接allure的安装和使用(windows环境)_allure windows-CSDN博客 第三步&#xff1a; 第四步&#xff1a; pytest 查看目前运行的测试用例有无错误 …

camunda最终章-springboot

1.实现并行流子流程 1.画图 2.创建实体 package com.jmj.camunda7test.subProcess.entity;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;import java.io.Serializable; import java.util.ArrayList; import java.util.List;Data …

打卡第6天----哈希表

每天进步一点点,滴水石穿,日积月累,不断提升。 数组和链表章节告一段落。开启哈希表相关的。 哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里 一、有效的字母异位词 leetcode题目编号:242 题目描述: 给定两个字符串 s 和 t ,编写一个函数…

压测引擎数据库设计(上)

压测引擎数据库设计&#xff08;上&#xff09; 引言 在当今快速发展的互联网时代&#xff0c;软件质量保证和性能测试变得尤为重要。自动化测试平台&#xff0c;提供了一套完整的解决方案&#xff0c;以确保软件产品在发布前能够满足性能和稳定性的要求。本文将深入探讨滴云自…

【AutoencoderKL】基于stable-diffusion-v1.4的vae对图像重构

模型地址&#xff1a;https://huggingface.co/CompVis/stable-diffusion-v1-4/tree/main/vae 主要参考:Using-Stable-Diffusion-VAE-to-encode-satellite-images sd1.4 vae 下载到本地 from diffusers import AutoencoderKL from PIL import Image import torch import to…

RIP环境下的MGRE网络

首先将LSP的IP地址进行配置 其他端口也进行同样的配置 将serial3/0/1配置25.0.0.2 24 将serial4/0/0配置35.0.0.2 24 将GE0/0/0配置45.0.0.2 24 进行第二步 R1与R5之间使用ppp的pap认证 在R5中进行配置 在aaa空间中创建账号和密码 将这个账号和密码使用在ppp协议中 然后…

【信息学奥赛】CSP-J/S初赛07 逻辑运算符与位运算

本专栏&#x1f449;CSP-J/S初赛内容主要讲解信息学奥赛的初赛内容&#xff0c;包含计算机基础、初赛常考的C程序和算法以及数据结构&#xff0c;并收集了近年真题以作参考。 如果你想参加信息学奥赛&#xff0c;但之前没有太多C基础&#xff0c;请点击&#x1f449;专栏&#…

BP神经网络的实践经验

目录 一、BP神经网络基础知识 1.BP神经网络 2.隐含层选取 3.激活函数 4.正向传递 5.反向传播 6.不拟合与过拟合 二、BP神经网络设计流程 1.数据处理 2.网络搭建 3.网络运行过程 三、BP神经网络优缺点与改进方案 1.BP神经网络的优缺点 2.改进方案 一、BP神经网络基…

C# modbus验证

窗体 还有添加的serialPort控件串口通信 设置程序配置 namespace CRC {public static class CRC16{/// <summary>/// CRC校验&#xff0c;参数data为byte数组/// </summary>/// <param name"data">校验数据&#xff0c;字节数组</param>///…

Nginx:负载均衡小专题

运维专题 Nginx&#xff1a;负载均衡小专题 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.csdn.net/…

docker nginx mysql redis

启动没有数据卷的nginx docker run -d -p 86:80 --name my-nginx nginx把/etc/nginx中的配置复制到宿主机 docker cp my-nginx:/etc/nginx /home/nginxlkl把/html 中的文件复制到宿主机 docker cp my-nginx:/etc/nginx /home/nginxlkl删除当前镜像 docker rm -f my-nginx重新起…

鸿蒙语言基础类库:【@ohos.uri (URI字符串解析)】

URI字符串解析 说明&#xff1a; 本模块首批接口从API version 8开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。开发前请熟悉鸿蒙开发指导文档&#xff1a;gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到。 导入…

汇川CodeSysPLC教程 Modbus变量编址

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

论文阅读 - Intriguing properties of neural networks

Intriguing properties of neural networks 经典论文、对抗样本领域的开山之作 发布时间&#xff1a;2014 论文链接: https://arxiv.org/pdf/1312.6199.pdf 作者&#xff1a;Christian Szegedy, Wojciech Zaremba, Ilya Sutskever, Joan Bruna, Dumitru Erhan, Ian Goodfellow,…

第二证券股市资讯:深夜!突然暴涨75%!

一则重磅收买引发医药圈轰动。 北京时间7月8日晚间&#xff0c;美股开盘后&#xff0c;美国生物制药公司Morphic股价一度暴升超75%。音讯面上&#xff0c;生物医药巨子礼来公司官宣&#xff0c;将以57美元/股的价格现金收买Morphic&#xff0c;较上星期五的收盘价溢价79%&…