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.mfbz.cn/a/457520.html

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

相关文章

K8S上安装LongHorn(分布式块存储) --use

要在 Kubernetes上安装 LongHorn&#xff0c;您可以按照以下步骤进行操作&#xff1a; 准备工作 将LongHorn只部署在k8s-worker5节点上。 给节点设置污点 $. kubectl taint nodes k8s-worker5 longhorn:PreferNoSchedule # 参考 # # 删除污点 # kubectl taint nodes k8s-w…

【趣味项目】一键生成LICENSE

【趣味项目】一键生成LICENSE 项目地址&#xff1a;GitHub(最新版本) | GitCode(旧版本) 项目介绍 一款用于自动生成开源项目协议的工具&#xff0c;可以通过 npm 进行安装后在命令行使用&#xff0c;非常方便 使用方式 npm install xxhls/get-license -gget-license --l…

MATLAB画图:错误使用plot无效的颜色或线型...

指定绘图颜色 - MATLAB & Simulink (mathworks.com) 使用matlab画图&#xff0c;想要使用其他颜色时&#xff0c;如想要从上面的颜色类型修改为下面的颜色类型 只需要在后面修改color属性即可 s1 plot(C3, LineWidth,2); s1.Color [0.8500 0.3250 0.0980]; hold on s2 …

node.js入门—day02

个人名片&#xff1a; &#x1f60a;作者简介&#xff1a;一名大二在校生 &#x1f921; 个人主页&#xff1a;坠入暮云间x &#x1f43c;座右铭&#xff1a;给自己一个梦想&#xff0c;给世界一个惊喜。 &#x1f385;**学习目标: 坚持每一次的学习打卡 文章目录 什么是单线程…

尚硅谷SpringBoot3笔记 (二) Web开发

Servlet&#xff0c;SpringMVC视频推荐&#xff1a;53_尚硅谷_servlet3.0-简介&测试_哔哩哔哩_bilibili HttpServlet 是Java Servlet API 的一个抽象类&#xff0c;用于处理来自客户端的HTTP请求并生成HTTP响应。开发人员可以通过继承HttpServlet类并重写其中的doGet()、do…

自然语言处理NLP:tf-idf原理、参数及实战

大家好&#xff0c;tf-idf作为文体特征提取的常用统计方法之一&#xff0c;适合用于文本分类任务&#xff0c;本文将从原理、参数详解和实际处理方面介绍tf-idf&#xff0c;助力tf-idf用于文本数据分类。 1.tf-idf原理 tf 表示词频&#xff0c;即某单词在某文本中的出现次数与…

蓝牙耳机链接电脑莫名奇妙关机问题(QQ浏览器)

蓝牙耳机连接电脑听歌的时候&#xff0c;如果听歌软件是暴风影音&#xff0c;或者其它播放器&#xff0c;蓝牙不会自动关机&#xff0c;但如果是QQ浏览器&#xff0c;蓝牙耳机经常莫名其妙的关机&#xff0c;时间间隔忽长忽短&#xff0c;没有规律&#xff0c;解决办法就是重启…

让el-input与其他组件能够显示在同一行

让el-input与其他组件能够显示在同一行 说明&#xff1a;由于el-input标签使用会默认占满一行&#xff0c;所以在某些需要多个展示一行的时候不适用&#xff0c;因此需要能够跟其他组件显示在同一行。 效果&#xff1a; 1、el-input标签内使用css属性inline 111<el-inp…

基于单片机的车载酒精含量自检系统设计与实现

摘要:调查显示,大约50%的交通事故与酒后驾车有关,酒后驾车已成为车祸致死的首要原因。为从根本上杜绝酒后驾车,设计了一款基于STC89C52 单片机的车载酒精含量自检系统,该系统能很好地解决酒驾问题,控制简单、使用方便,具有很好的应用价值。 关键词:STC89C52 单片机;车…

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

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

hadoop伪分布式环境搭建详解

&#xff08;操作系统是centos7&#xff09; 1.更改主机名&#xff0c;设置与ip 的映射关系 hostname //查看主机名 vim /etc/hostname //将里面的主机名更改为master vim /etc/hosts //将127.0.0.1后面的主机名更改为master&#xff0c;在后面加入一行IP地址与主机名之间的…

PostgreSQL开发与实战(6.3)体系结构3

作者&#xff1a;太阳 四、物理结构 4.1 软件安装目录 bin //二进制可执行文件 include //头文件目录 lib //动态库文件 share //文档以及配置模版文件4.2 数据目录 4.2.1 参数文件 pg_hba.conf //认证配置文件 p…

给电脑加硬件的办法 先找电脑支持的接口,再买相同接口的

需求&#xff1a;我硬盘太小&#xff0c;换或加一个大硬盘 结论&#xff1a;接口是NVMe PCIe 3.0 x4 1.找到硬盘型号 主硬盘 三星 MZALQ512HALU-000L2 (512 GB / 固态硬盘) 2.上官网查 或用bing查 非官方渠道信息&#xff0c;不确定。

HTTP代理的特性、功能作用是什么样的?

在当今互联网时代&#xff0c;HTTP代理作为网络通信中的一项重要技术&#xff0c;在各行各业都有着广泛的应用。然而&#xff0c;对于许多人来说&#xff0c;HTTP代理的特性和功能作用并不十分清晰。在本文中&#xff0c;我们将深入探讨HTTP代理的各种特性和功能&#xff0c;帮…

报表生成器FastReport .Net用户指南:关于脚本(上)

FastReport的报表生成器&#xff08;无论VCL平台还是.NET平台&#xff09;&#xff0c;跨平台的多语言脚本引擎FastScript&#xff0c;桌面OLAP FastCube&#xff0c;如今都被世界各地的开发者所认可&#xff0c;这些名字被等价于“速度”、“可靠”和“品质”,在美国&#xff…

探索编程新纪元:Code GeeX、Copilot与通义灵码的智能辅助之旅

在人工智能技术日新月异的今天&#xff0c;编程领域的革新也正以前所未有的速度推进。新一代的编程辅助工具&#xff0c;如Code GeeX、Copilot和通义灵码&#xff0c;正在重塑开发者的工作流程&#xff0c;提升编程效率&#xff0c;并推动编程教育的普及。本文将深入探讨这三款…

如何在Windows 10上打开和关闭平板模式?这里提供详细步骤

前言 默认情况下&#xff0c;当你将可翻转PC重新配置为平板模式时&#xff0c;Windows 10会自动切换到平板模式。如果你希望手动打开或关闭平板模式&#xff0c;有几种方法可以实现。​ 自动平板模式在Windows 10上如何工作 如果你使用的是二合一可翻转笔记本电脑&#xff0…

《小程序从入门到入坑》框架语法

前言 哈喽大家好&#xff0c;我是 SuperYing&#xff0c;我们继续小程序入门系列&#xff0c;本文将对小程序框架语法进行比较全面的介绍。在《小程序从入门到入坑》简介及工程创建中&#xff0c;我们提到小程序项目结构&#xff0c;主要包括 app.json&#xff0c;app.js&…

某夕夕商品数据抓取逆向之webpack扣取

逆向网址 aHR0cHM6Ly93d3cucGluZHVvZHVvLmNvbQ 逆向链接 aHR0cHM6Ly93d3cucGluZHVvZHVvLmNvbS9ob21lL2JveXNoaXJ0 逆向接口 aHR0cHM6Ly9hcGl2Mi5waW5kdW9kdW8uY29tL2FwaS9naW5kZXgvdGYvcXVlcnlfdGZfZ29vZHNfaW5mbw 逆向过程 请求方式&#xff1a;GET 参数构成 【anti_content】…

爬虫入门到精通_实战篇12(使用Redis+Flask维护动态Cookies池)

1 目标 为什么要用Cookies池 网站需要登录才可爬取&#xff0c;例如新浪微博爬取过程中如果频率过高会导致封号需要维护多个账号的Cookies池实现大规模爬取 Cookies池的要求 自动登录更新定时验证筛选提供外部接口 2 流程框架 首先&#xff0c;需要有一个账号队列&#xf…