【Linux】线程(一)

谈论之前需要先谈论一些线程的背景知识

其中就有进程地址空间,又是这个让我们又爱又恨的东西。

注意:全篇都是在32位的情况下进行的

背景知识:

地址空间:

说在前边,OS通常分为4个核心模块:执行流管理,文件系统管理,文件管理与IO管理。

在这先要涉及到对内存的管理。

内存:

以下是一张内存与磁盘的形象图。
在这里插入图片描述
在没接触内存管理之前我们通常是认为内存是直接一整块的。
现在我们要知道内存其实不是一整块的,而是分成了一个大数组,数组每个大小4kb!每个4kb叫做页框或页帧。
在这里插入图片描述
当然,这个是可以进行更改的,但是需要将OS重新编译一份出来,另外这个数字是由计算机科学家研究出来的。

我们的这个4KB是不是看着很熟悉呢?
4KB也正是磁盘文件与系统进行IO的基本单位。
在OS中的巧合都是精心设计的~

结论:我们磁盘中的可执行程序也是文件,是文件就有对应的inode。我们磁盘中已经天然的按4kb,比如说我们文件存放自己的数据就在datablock[]中,这个就是以4kb进行划分的,所以在进行磁盘IO时,所谓的加载就是把数据块加载到指定的内存块。


那么现在就有一个问题了:当父子进程共享了一个全局变量int类型,进行写时拷贝时是进行4字节的拷贝还是4kb呢?

答案是4kb,这遵循了一个局部性的原则,当你本行的数据被修改,那么下一行会有90的概率也被修改,避免了太多的拷贝,OS也是很忙的。
所以其实我们C语言malloc4字节其实也是申请了4kb,只是我们只能只用4字节(很粗略的理解)
我们的共享内存所以一般也都是申请4096个字节的整倍数…


那么是怎么进行划分的呢,那么多4kb是如何管理的呢?
比如每个内存块被占用了多少,被谁占用了…

所以需要进行管理。
假设我们是4GB,那么大约是一百万个页框,更准确来说是1048576,每个页框都有自己的使用情况。

在这里插入图片描述

最底层使用数组进行管理起来,每个page都有自己对应的下标–>使用对应的下标×4kb == 页框地址

所以对内存的管理就变为了对数组的增删查改。
那么这个数组大约多大呢?
假设每一份是1byte,那么大约是1mb。
但是实际上一份大约是十几byte。那么就是十几MB就可以管理起来这个数组了。


页表:

真实的页表是什么样子?虚拟地址又是如何转化到物理地址?
在还没有真正接触页表之前,我们通常会这么认为:
在这里插入图片描述
我们先来说一下第二个问题:
我们进行地址的转化是需要与CPU进行配合的。
CPU中接收到虚拟地址+MMU–>物理地址。

再来看一下第二个问题:
我们的虚拟地址有32位,那么每个虚拟地址就有32个比特位。

在这里插入图片描述
其中前十比特位用来索引第一张页表,也就是第一张页表的下标。
在这里插入图片描述
那么第一张页表中存放的是什么?
是第二张页表的地址。

在这里插入图片描述
所以中10位也就是第二张页表的索引(下标咯)。
在这里插入图片描述
那么后12位代表着什么?
我们想到每个页框的大小为4kb->4096字节->[0, 4095],而我们的后12位正好也是[0, 4095],所以我们的低12位就是每个页框的偏移量!
在这里插入图片描述
所以我们页表的本质是进行搜索页框!

这种分配方案叫做二级页表,大大减少了内存。

口说无凭,我们计算一下:

首先第一张页表叫做页目录。
存的是第二张页表的地址。
页目录的大小 = 4字节×1024 = 4kb。

第二张页表叫做页表
页表中存的是page的起始地址,
页表大小 = 4字节 × 1024 × 1024 = 4mb。

所以总大小为4mb + 4kb就是页表了!

何况也是按照拉满的进行计算,实际是 <= 这个大小的。

注意:page这个管理内存的数据结构与页表是相辅相成,
它们之间虽然在逻辑上有间接的联系(都是用来管理物理内存的不同方面),但实际上它们的数据内容和直接操作是分离的。

但是我们现在仍然只能获得一个字节的地址,一个int都4个字节,怎么读取一个int的数据呢?
所以需要类型?
汇编会告诉CPU你的对象类型是什么。

所以现在还差一个问题。
当CPU获得虚拟地址时,MMU可以进行运算找到页框。但是这个过程中我们得有页目录的地址啊,才能进一步的运算,地址怎么获得?
在CR系列的寄存器中。
在这里插入图片描述
CR3就负责存页目录的地址。

基于以上理解文件缓冲区与虚拟地址:

文件缓冲区:

文件缓冲区实际上就是从struct page memory这个数组中挑几个page与struct file进行关联,这就是文件缓冲区,至于怎么关联的是依靠字典树,了解一下即可。

虚拟地址:

我们以正文代码段为例:在这里插入图片描述
我们的正文代码实际上是一段范围,如果有20个函数,那我们就可以将正文分成20部分。
是不是很抽象的感觉,没事,我们来好好的捋一捋。
首先我们知道函数是有地址的,是一批代码的入口地址;函数的每一行也都有地址,而我们的函数都是连续的,且编译是以平坦模式的绝对编址,所以最终每个函数都是一个一个连续的代码块。

那么正文就理所当然的可以拆分为20分,本质就是拆分页表,也就是一种资源。

线程:

那么就来看一看一下官方的线程定义:

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

果然是十分抽象~

linux下的线程:

线程与进程实际上是有密不可分的关系。
所以我们先从进程开始了解线程。

这是我们以前接触的进程:
进程 = 内核数据结构 + 代码和数据
在这里插入图片描述
每一个进程都有属于自己的地址空间与页表,各自有各自的数据。

但是如果我们创建几个没有地址空间,页表与全新代码的进程呢?
在这里插入图片描述

我们将正文代码分为3部分,分别给task_structA B C。
让这3个分别执行代码的一部分,他们共享地址空间,这叫做“linux下的线程”。

所以现在我们知道了定义的第一点:线程在进程内部运行,也就是在地址空间内运行。

与进程的澄清:

进程 = 内核数据结构 + 代码和数据。

也就是说下图这个整体是一个进程。
在这里插入图片描述
而线程是其中一个task_struct与虚拟地址+页表+代码数据(绿色的不是)。
在这里插入图片描述
站在OS的视角上,我们可以给进程下一个定义:是承担分配系统资源的实体。

怎么理解?
这就像进程是一个家庭,它有着国家分配的资源;而线程是家庭成员。

爸妈执行上班的代码,孩子执行上学的代码,爷爷奶奶执行养老的代码。
他们为了一个共同的目的,那就是将日子过好。

所以我们之前理解的进程就像是家庭中只有一个成员。

win下的进程:

如果我们要设计线程,那么每个进程可能都有很多线程,有的是被创建,有的被销毁,有的被调度,所以就要被管理。win下的线程就是这样设计的。
它有着一个描述线程的tcb,还有一个将tcb组织起来的数据结构

struct tcb // thread control bolock
{
	// id 优先级 状态 上下文
	// ...
}

在这里插入图片描述

与linux进行比较:

但是linux并没有这么做,linux选择了复用pcb,因为tcb需要的pcb几乎都已经准备好了,于是用pcb统一表示执行流,这样就不需要单独设计了!

那么哪个OS的做法更优呢?
win:实现了真正的tcb。
linux:复用,用进程来模拟线程。

我们认为Linux更优秀,因为他的代码量少,成本低,健壮性高。


那么当CPU调度时,需要关注task_struct是进程还是线程吗?
答案是不需要的,CPU只关心如何执行,每个task_struct都是一个执行流,他已经包含了自己执行需要的东西。

所以CPU看到的每一个执行流都是 <= 进程的,linux中的线程叫做轻量级进程!

所以现在对于定义的第二点我们也能理解了。

代码:

说了这么久的理论,我们总归还是要回归代码的。
在这里插入图片描述

那么线程如何进行创建?
参数解释:
thread:是线程id。
attr:我们不管,设置为nullptr即可。
start_routine:是我们新线程去执行的函数。
arg:传给函数的参数。

在这里插入图片描述

现象:
在这里插入图片描述
发现只有一个pid。
利用ps | ajx也是如此:
在这里插入图片描述
原因在于线程都是在同一个进程内部。

但是我们也可以查到线程:ps -aL 即可
在这里插入图片描述
我们发现有一个叫做LWP的东西,这个其实就是轻量级进程的意思(light weight process),下面的数字是线程的唯一标识符。

我们说过CPU调度时是以线程为单位的,那么就肯定看的是LWP。
其中PID = LWD的那一个线程叫做主线程。

有了多进程为什么还要多线程?

这里主要有3个原因造成。
创建
进程创建时需要PCB,地址空间,页表,加载代码和数据,还有标准输入输出…成本很高;
但是线程的创建只需要创建一个PCB,指向地址空间即可、
调度
进程的切换要切换上下文,切换页表…
而线程由于是共享地址空间,所以页表那些东西就不要切换了
死亡
进程的死亡需要释放PCB,虚拟地址空间,页表…
线程只进行删除对应的PCB即可。

但是对于调度的理解还需要再加深一下:
我们切换页表什么的仅仅修改一下CR3寄存器即可,无非就是修改了几个寄存器,实际上并不会有很大的优势。

不过调度效率高这是事实,这其中与硬件还有密不可分的关系。
答案就在于cache。
在这里插入图片描述
我们的CPU为了调度进程时为了更快的访存效率,往往会存在cache,这其实依据了局部性原因,比如当我们访问第五行时,CPU会把围绕这行代码的高频数据预先加载到cache中,这叫做热数据,当所以PU进行访存时会先在cache中寻找,找到直接拿,找不到在切换cache。
这意味着如果是进程间的切换,对与cache中的数据肯定是用不到的,因为进程具有独立性,所以需要将cache的数据全部替换。
而线程的切换往往对于cache是用的到的,避免了大量的替换,所以效率更高!

重新理解代码被划分:

我们在背景知识的最后一段说了一段比较抽象的话,关于代码被划分的问题。

我们的程序经过编译后,所谓的线程执行的方法其实就变成了一块代码块,所以县城区执行函数本质就是拥有了正文代码的一部分—>小块虚拟地址空间的范围—>页表的一部分—>物理内存—>也就是资源。

重新梳理一遍概念:

Linux线程概念:

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
    在这里插入图片描述

线程的优点:

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

这里对后两个做一下解释:

什么叫做计算密集型?
计算加密解密,压缩文件等工作。
要注意,并不是线程越多越好,如果太多的话反而增加切换的成本,需要一个合适的线程数量,那么这个合适的量一般是多少?
一般你的CPU是几核就创建几个线程,有几率一个CPU执行一个线程,做到真正的并发。

什么叫做IO密集型?
大部分时间都在等待外部设备(如硬盘、网络)操作完成的任务,这时的CPU往往处于空闲状态。
在I/O密集型任务中,通过创建多线程,可以使得在某些线程等待I/O时,其他线程能够继续执行,从而重叠I/O等待时间与计算时间,提高了处理器的利用率和任务的执行效率

线程的缺点:

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

这里写一段代码便于理解。
比如我们有多个线程。
在这里插入图片描述
现象:
在这里插入图片描述
当一个线程收到信号,那么整个进程就会被终止,因为线程是进程的一部分。

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

这个应该怎么理解?
代码:在这里插入图片描述
现象:
在这里插入图片描述
由于共享地址空间,所以当其中一个线程更改了一个全局公用的数据,那么其他线程也会受到影响。

线程异常:

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

线程用途:

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

关于线程资源的共享:

线程共享进程数据,但也拥有自己的一部分数据:

  • 线程ID
  • 一组寄存器
  • errno
  • 信号屏蔽字
  • 调度优先级

其中最重要的是一组寄存器

一组寄存器:保存硬件的上下文–>线程是可以动态运行的。

栈:如果公用一个栈,那么每个线程的临时变量都会在一个栈中,有的线程压栈,有的出栈,最终造成混乱,所以线程在运行的时候形成的各种临时变量会被保存在自己的栈区

为什么编译时要加-lpthread?

现在我们要解决最后一个问题:
我们已经说过,linux中是没有线程的,只有轻量级进程。

虽然linux没有线程,但是他有与线程一致的轻量级进程,为了自己的纯粹性,他不会提供线程的接口。

但是用户在学习linux时只会学习线程的操作,所以中间势必会有一层软件层–>ptread库,他是对LWP的封装,按照线程的接口设计。

在这里插入图片描述
所以linux中的线程是用户级别的。
win下的线程是内核级。

本篇完~

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值