Linux线程概念

目录

一、页表详解

1.地址的属性

2.页框

3.页表录和页表项

二、认识线程

1.线程的概念

2.轻量级进程

三、线程的简单控制

1.线程的创建

2.PID和LWP

3.线程异常

4.线程的资源

(1)共享的资源

(2)独有的资源

5.线程的特点


一、页表详解

我们虽然已经知道了页表可以储存虚拟地址和物理地址的映射,而且转换物理空间的过程是页表和MMU共同完成的,但是我们还是很难具象性地理解这个数据结构。看完下面这部分就相信我们对它的理解就更具体直观了。

1.地址的属性

#include<iostream>
using namespace syd;
int main()
{char* str = "hello world";*str = 'H';return 0;
}

这段代码运行会报错,因为str指向的虚拟地址在字符常量区,字符常量区的数据不允许用户修改。

操作系统能知道一个地址可否被读写的原因是:页表中储存了各个地址的属性。

那我们不妨画一个页表的示意图。

可以看到每个虚拟地址都有自己的属性,而虚拟地址、物理地址及其属性组成的那一行数据就称为条目。

  • U/K权限:U表示用户(user),K表示内核(kernal)。
  • RWX权限:当前身份(用户或者内核)对当前地址的读,写执行权限。

代码在对str的地址进行写操作时,会先经过页表寻找物理地址。但是页表发现这是一个写操作且该地址不允许修改,此时MMU就发送信号,程序报错。

2.页框

32位系统的物理内存理论上有4GB,这4GB又被分成了许多大小为4KB的小空间,被叫做页框。

这些页框当然需要操作系统的管理,采用的方式同样为先描述,后组织。

我们可以用一个结构体struct_Page管理一个页框。

struct_Page
{//4KB空间的属性
}

如果我们再将多个结构体对象放在一个数组中:struct_Page mem[];

此时所有的页框就被管理起来了,操作系统会用一个叫做伙伴系统的调度算法管理这些数组。如果对这个算法有兴趣,可以自行查阅资料了解,不再赘述。

3.页表录和页表项

既然页表能够覆盖4GB的地址空间,那么它就会应该有2的32次方个条目,每个条目包含了物理地址、虚拟地址和其他属性。

那我们假设一个条目的大小是10Bytes,光一个页表就应该占10×2^32=40GB的空间,早就远远超过了系统物理内存,所以页表的设计一定不是这样的。

实际上,页表是由页目录和页表项组成的。那么通过一个32比特位的虚拟地址又是怎么找到一个物理地址的呢?

由于一个地址总共32个比特位,我们不妨把它分为三个部分,前面的10个比特位,中间10个比特位,结尾12个比特位。

其中前10位是页目录的下标,对应红框的十个0,通过这个下标0就可以访问到页目录中的第一个条目。由于页目录中储存着页表项的地址,通过下标就能找到对应的页表项。

10个比特位意味着页目录的下标范围是0~1023,也就是页目录最多有1024个条目。

32个比特位的中间10位是页表项的下标,同样可以访问页表项中的条目。由于页表项中储存着物理内存的页框起始地址,这样就可以通过下标找到物理内存的对应页框。

同样,一个页表项最多也有1024个条目,指向1024个页框。

32个比特位中的最后12位,作为偏移量,在页框的起始地址加上偏移量就可以找到具体数据在内存中的地址。

这也是为什么页框和页帧的大小设置为4KB的原因,因为以一个字节为单元,最低的12个比特位偏移量最多能覆盖4KB的空间。

那我们不妨再次总结一下整个映射过程:

首先,高10位做下标,从页目录中找到对应页表项的地址;

然后,中间10位做下标,从页表项中找到对应页框的起始物理地址;

最后,低12位做偏移量,在起始物理地址上加上偏移量找到具体的物理地址。

当然,页目录和页表项同样采用先描述再组织的方式进行管理。每创建一个进程就会有一个页目录被创建,只有建立映射的虚拟地址才会建立页表项进行管理。采用这种方式,大大减少了对内存的消耗。

地址空间是进程看到的资源,每个进程都认为4GB的资源都被自己占有。而页表映射物理地址的能力决定了进程真正拥有的资源。合理对地址空间和页表进行资源划分,就可以对一个进程所有的资源进行分类。

二、认识线程

1.线程的概念

线程是进程内部的一个执行流。

这个概念貌似抽象,但我举个例子就好理解了。比如说,一个三口之家,爸爸在公司上班,妈妈在家打扫卫生,孩子在学校上学。虽然三个人各自干着不同的事,但它们依然在家庭这个屋檐下。

在这个例子中三个人都在做自己的事情,类比到进程中就是一个进程有三个执行流。而大家都属于同一个家,类比在进程中就是多个执行流会共享进程的资源。到这里,我们把“进程内部的执行流”叫做线程。

其实我们之前学到的进程只有一个执行流,也就是一个进程中只有一个线程。这个最初的进程执行流叫做主线程,之后创建的执行流都叫做新线程,主线程与新线程同属一个进程。

还是上面的例子,家庭成员共享家庭资源,家庭成员之间既可以有共享资源,也可以有私人秘密。

同样,所有的线程都共用一个地址空间,所以线程间可能会互相影响。

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确地说,线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
  • 线程共享进程数据,但也拥有自己的一部分数据,比如线程ID、一组寄存器、栈结构、errno、信号屏蔽字与调度优先级

2.轻量级进程

在操作系统中有着这么多的线程,操作系统就必须对其进行管理。

根据先描述,再组织的原则,首先我们要为线程设计专门描述其属性的数据结构——线程控制块(TCB),在设置好TCB后,再用一些链表这样的数据结构对TCB进行管理,还需要编写管理TCB的管理算法,这样就能做到对线程的管理了。

在开发者们设计Linux时,它们发现线程和进程在属性上非常相似, 而且管理算法也基本一致,所以他们就以作为PCB的task_struct结构体直接用到了线程TCB上,管理PCB的算法也能直接用于管理TCB。

当同时有进程和线程运行时,由于进程PCB和线程TCB底层都是task_struct结构体,所以CPU也不能识别哪个是进程,哪个是线程,就统一当进程处理了。所以在Linux中的进程和线程就统一叫做轻量级进程,也就是说在Linux中是不存在线程的概念的。

三、线程的简单控制

1.线程的创建

int pthread_create(pthread_t* thread, const pthread_attr_t *attr, void*(*start_routine)(void*), void* arg);

头文件:pthread.h

功能:创建一个线程。

参数:pthread_t* thread是线程标识符指针,pthread_t是线程标识符的类型,它是一个输出型参数。const pthread_attr_t* attr是线程属性,现在设成nullptr就好。void* (*start_routine)(void ):是一个函数指针,线程会执行该函数中的代码。void* arg是线程执行函数的形参。

返回值:成功返回0,失败返回的错误码。

#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;void* start_routine(void* args)
{char* arg = (char*)args;while(1){cout  << arg << ":我是一个新线程"<<endl;sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, start_routine, (void*)"new pthread");assert(n == 0);while(1){cout << "我是主线程" << endl;sleep(1);}return 0;
}

我们直接编译,但是它告诉我们pthread_create编译器不认识。

这是因为在Linux内核中不存在线程概念,更没有TCB数据结构及其管理算法,而我们所说的线程,都是在宏观层面面对所有操作系统。

Linux操作系统中当然也没有提供创建线程的系统调用,它只能提供创建轻量级进程的系统调用。

但是大部分程序员都已经习惯了创建线程,那该怎么办呢?

所以我们需要在创建线程时调用一个线程库,库中会通过一些系统调用创建轻量级进程。在用户层程序员创建线程,Linux内核中创建轻量级进程,双方的要求都能满足。

因为这个线程库是所有Linux操作系统必须自带的,所以也叫做原生线程库。

我们在编译指令后加上 -lpthread链接原生线程库,程序就正常运行了。new pthread作为参数也被传递到新线程函数中。

输入ldd加可执行程序可以看到它的链接属性。

可以看到原生线程库时动态链接的,我们再看看这个被链接的库的位置。

我们发现被链接的这个文件其实是原生线程库libpthread-2.17.so的软连接。

2.PID和LWP

我们再次运行上面的程序,输入ps ajx | grep ...的指令进行进程查看。

和我们的讲解一致,虽然现在有两个线程在运行,但我们只能找到一个进程,pid为2874(下面那个是grep指令的进程)

输入ps -aL(L必须大写)查看线程

此时可以看到sp有两个线程,它们的PID值都为2874,但LWP值为2874和2875。

其中PID是进程标识符,LWP(Light Weight Process)是轻量级进程标识符。由于第一个线程的LWP和PID是一样的,所以这个线程就被叫做主线程。而第二个线程LWP和PID不一样,这个线程叫做新线程,PID和LWP的异同才是判断线程是否为主线程的标志。

我们说过,CPU在调度task_struct时将它们一律看作进程,不同进程的PID一定不同,而同进程的线程PID是相同的,所以只有LWP才能作为标识符标识执行流的唯一性。

因为CPU每次只能处理一个执行流,而进程可以存在一个或多个执行流。CPU通过LWP识别执行流,这个执行流放在操作系统就是线程,所以线程是CPU调度的基本单位

而对于之前学过的进程而言,由于它只有一个执行流,LWP和PID一致,所以我们就不会提及LWP。

既然说到了进程,在Linux下进程才有独立的页表和地址空间,所以进程是资源分配的基本单位

结论:LWP是轻量级进程标识符,也相当于线程的标识符,PID与LWP的异同决定改线程是否为主线程。进程是资源分配的基本单位,线程是CPU调度的基本单位。

3.线程异常

单个线程如果出现除零,野指针等问题导致线程崩溃,而线程是进程的一部分,进程也会出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。相当于只要有一个人出错了,整个团队的工作就都停止了。这也是我们向进程发送2号信号可以终止所有线程的原因。

如果程序容易出现这样的问题,我们称这个程序健壮性或者鲁棒性较差。

下面的代码就能实现上述结果:

#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;void* start_routine(void* args)
{sleep(3);int* p = nullptr;*p = 1;
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, start_routine, (void*)"new pthread");assert(n == 0);while(1){cout << "我是主线程" << endl;sleep(1);}return 0;
}

4.线程的资源

(1)共享的资源

所有线程都共享一个虚拟地址空间,一个页表,而且进程中的绝大部分资源线程都共享。

我们创建一个全局变量,而且让主线程也运行线程函数。

#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;
int gal_num = 0;void* start_routine(void* args)
{char* arg = (char*)args;while(1){cout  << arg << ":我是一个线程" << ++gal_num << endl;sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, start_routine, (void*)"pthread2");assert(n == 0);start_routine((void*)"pthread1");return 0;
}

我们发现两个线程都可以使用同一个函数,说明代码区共享,说明进程的 多个线程共享进程地址空间,两个线程可以改变同一个变量,又说明它们共享页表。

(2)独有的资源

线程也有自己独有的资源。比如,描述它们的task_struct结构体私有,线程运行的上下文数据私有,栈结构独立私有,可以在线程内部创建临时变量。

虽然我们可理解栈结构私有的意义,但是所有线程都用同一块虚拟地址空间的栈区,又怎么能产生独立栈结构呢?

这是因为,原生线程库的pthread_creat使用了系统调用clone,而clone是可以创建子进程的。这里创建的子进程是轻量级进程,没有独立的虚拟地址空间。

我们查看clone的声明:int clone(int (*fn)(void *), void *stack, int flags, void arg, .../ pid_t *parent_tid, void *tls, pid_t *child_tid */ );

clone中有一个参数void* stack,这个参数就是用来设置这个子进程栈空间的。

5.线程的特点

线程的优点:

  • 创建一个新线程不需要独立的地址空间和页表,代价要比创建一个新进程小得多。

那这个代价小了多少呢?

进程切换的工作包括:PCB切换、上下文数据切换、转变虚拟地址空间、转换页表

而线程切换仅包括:PCB切换和上下文数据切换

这里还有一个细节:进程切换需要重新加载高速缓存的内容,而线程切换高速缓存(cache)的内容大部分都可以保留。

CPU在使用数据时,会先去高速缓存cache中拿(详见之前的计算机数据存储模式),如果不命中(不存在该数据),cache就会根据局部性原理将CPU需要的数据从内存加载到cache中,尤其使用频率高的热点数据会一直放在cache中。

cache中的内容是根据虚拟地址和页表缓存进来的,所以同一个进程的线程之间这些数据都是可以共用的。免去了加载数据的过程,大大节省了时间,也节省了操作系统的大量工作。

  • 与进程之间的切换相比,线程之间的切换只需要切换PCB和上下文数据,地址空间和页表都不用切换,工作量更少。
  • 线程占用的资源比进程少得多。
  • 能充分利用多处理器的可并行数量。
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中。
  • 实现I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点:

  • 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多。

还有一点要补充:因为所有PCB都共享地址空间,所以线程能够看到进程的所有资源,线程间通信成本低。但地址空间存在大量的临界资源,势必需要使用各种互斥和同步机制保证临界资源的安全。

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

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

相关文章

mysql 存储引擎系列 (一) 基础知识

当前支持存储引擎 show engines&#xff1b; 显示默认存储引擎 select default_storage_engine; show variables like ‘%storage%’; 修改默认引擎 set default_storage_enginexxx 或 set default_storage_enginexxx; my.ini 或者 my.cnf ,需要重启 服务才能生效 systemctl …

cortex-A7核LED灯实验--STM32MP157

实验目的&#xff1a;实现LED1 / LED2 / LED3三盏灯工作 一&#xff0c;分析电路图 1&#xff0c;思路 分析电路图可知&#xff1a; 网络编号 引脚编号 LED1 PE10 LED2 > PF10 LED3 > PE8 2&#xff0c;工作原理&#xff1a; 写1&#xff1a;LED灯亮&#xf…

防雷检测综合应用方案

防雷检测是指对建筑物的防雷装置进行定期或不定期的检测&#xff0c;以评估其性能和安全状况&#xff0c;发现并消除隐患&#xff0c;保障人身和财产安全的一项重要工作。防雷检测的内容包括对接闪器、避雷带、引下线、接地装置、等电位联结、避雷器等部件的形式、位置、连接、…

RecyclerView面试问答

RecycleView 和 ListView对比: 使用方法上 ListView:继承重写 BaseAdapter,自定义 ViewHolder 与 converView优化。 RecyclerView: 继承重写 RecyclerView.Adapter 与 RecyclerView.ViewHolder。设置 LayoutManager 来展示不同的布局样式 ViewHolder的编写规范化,ListVie…

Spring security报栈溢出几种可能的情况

今天在运行spring security的时候&#xff0c;发现出现了栈溢出的情况&#xff0c;总结可能性如下&#xff1a; 1.UserDetailsService的实现类没有加上Service注入到容器中&#xff0c;导致容器循环寻找UserDetailsService的实现类&#xff0c;最终发生栈溢出的现象。 解决方法…

Redis 7 第三讲 数据类型 进阶篇

⑥ *位图 bitmap 1. 理论 由0和1 状态表现的二进制位的bit 数组。 说明:用String 类型作为底层数据结构实现的一种统计二值状态的数据类型 位图本质是数组,它是基于String 数据类型的按位操作。该数组由多个二进制位组成,每个二进制位都对应一个偏…

Java进阶(6)——抢购问题中的数据不安全(非原子性问题) Java中的synchronize和ReentrantLock锁使用 死锁及其产生的条件

目录 引出场景&#xff1a;大量请求拥挤抢购事务的基本特征ACID线程安全的基本特征 加锁(java)synchronized锁ReentrantLock锁什么是可重入锁&#xff1f;如何保证可重入 滥用锁的代价&#xff1f;&#xff08;死锁&#xff09;死锁的四个必要条件死锁的案例 总结 引出 1.大量请…

基于SpringBoot的员工(人事)管理系统

基于SpringBoot的员工&#xff08;人事&#xff09;管理系统 一、系统介绍二、功能展示三.其他系统实现五.获取源码 一、系统介绍 项目名称&#xff1a;基于SPringBoot的员工管理系统 项目架构&#xff1a;B/S架构 开发语言&#xff1a;Java语言 前端技术&#xff1a;BootS…

Go 第三方库引起的线上问题、如何在线线上环境进行调试定位问题以及golang开发中各种问题精华整理总结

Go 第三方库引起的线上问题、如何在线线上环境进行调试定位问题以及golang开发中各种问题精华整理总结。 01 前言 在使用 Go 语言进行 Web 开发时&#xff0c;我们往往会选择一些优秀的库来简化 HTTP 请求的处理。其中&#xff0c;go-resty 是一个被广泛使用的 HTTP 客户端。…

运用亚马逊云科技Amazon Kendra,快速部署企业智能搜索应用

亚马逊云科技Amazon Kendra是一项由机器学习&#xff08;ML&#xff09;提供支持的企业搜索服务。Kendra内置数据源连接器&#xff0c;支持快速访问Amazon S3、AmazonRDS、AmazonFSX以及其他外部数据源&#xff0c;帮助用户自动提取文档并建立索引。Kendra支持超过30多种多国语…

【真题解析】系统集成项目管理工程师 2022 年下半年真题卷(案例分析)

本文为系统集成项目管理工程师考试(软考) 2022 年下半年真题&#xff08;全国卷&#xff09;&#xff0c;包含答案与详细解析。考试共分为两科&#xff0c;成绩均 ≥45 即可通过考试&#xff1a; 综合知识&#xff08;选择题 75 道&#xff0c;75分&#xff09;案例分析&#x…

无涯教程-分类算法 - 逻辑回归

逻辑回归是一种监督学习分类算法&#xff0c;用于预测目标变量的概率&#xff0c;目标或因变量的性质是二分法&#xff0c;这意味着将只有两种可能的类。 简而言之&#xff0c;因变量本质上是二进制的&#xff0c;其数据编码为1(代表成功/是)或0(代表失败/否)。 在数学上&…

reactantd(12)动态表单的默认值问题

最近遇到一个需求是有一个表单可以输入各种信息&#xff0c;然后还需要有一个编辑功能&#xff0c;点击编辑的时候需要把当前数据填入到表单里面。在网上查了很多种方法&#xff0c;然后我的思路是使用initialValues搭配setState()使用。默认值都为空&#xff0c;然后点击单条数…

VMware虚拟机---Ubuntu无法连接网络该怎么解决?

在学习使用Linux系统时&#xff0c;由于多数同学们的PC上多是Windows系统&#xff0c;故会选择使用VMware创建一个虚拟机来安装Linux系统进行学习。 安装完成之后&#xff0c;在使用时总是会遇到各种各样的问题。本片随笔就主要针对可能出现的网络问题进行一个总结&#xff0c;…

Git仓库简介

1、工作区、暂存区、仓库 工作区&#xff1a;电脑里能看到的目录。 暂存区&#xff1a;工作区有一个隐藏目录.git&#xff0c;是Git的版本库&#xff0c;Git的版本库里存了很多东西&#xff0c;其中最重要的就是称为stage&#xff08;或者叫index&#xff09;的暂存区&#xf…

stm32读写片内flash项目总结(多字节读写tongxindu)

1.flash操作驱动程序 a头文件 #ifndef FLASH_H #define FLASH_H #include “stm32f4xx.h” #define BOARD_NUM_ADDR 0x0800C000 #define STM32_FLASH_BASE 0x08000000 //STM32 FLASH的起始地址 #define FLASH_WAITETIME 50000 //FLASH等待超时时间 //FLASH 扇区的起始地址…

idea --Git Commit Template插件

Git Commit Template是一款免费的IntelliJ IDEA插件&#xff0c;用于提供Git提交模板。该插件可以帮助开发者编写规范的Git提交信息&#xff0c;提高代码管理效率。 首先安装插件&#xff1a; 使用Git Commit Template插件: 注&#xff1a;long description和Breaking changes…

开源文库系统moredoc

什么是 moredoc &#xff1f; moredoc 中文名 魔豆文库&#xff0c;是基于 golang 开发的类似百度文库、新浪爱问文库的开源文库系统&#xff0c;支持 TXT、PDF、EPUB、MOBI、Office 等格式文档的在线预览与管理&#xff0c;为 dochub 文库(github, gitee &#xff09;的重构版…

代码随想录算法训练营第四十九天 | 121. 买卖股票的最佳时机,122.买卖股票的最佳时机II

代码随想录算法训练营第四十九天 | 121. 买卖股票的最佳时机&#xff0c;122.买卖股票的最佳时机II 121. 买卖股票的最佳时机122.买卖股票的最佳时机II 121. 买卖股票的最佳时机 题目链接 视频讲解 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股…

英特尔开始加码封装领域 | 百能云芯

在积极推进先进制程研发的同时&#xff0c;英特尔正在加大先进封装领域的投入。在这个背景下&#xff0c;该公司正在马来西亚槟城兴建一座全新的封装厂&#xff0c;以加强其在2.5D/3D封装布局领域的实力。据了解&#xff0c;英特尔计划到2025年前&#xff0c;将其最先进的3D Fo…