腹泻原因

注册

 

发新话题 回复该主题

硬核操作系统讲解 [复制链接]

1#

1冯诺伊曼体系

1.1冯诺伊曼体系简介

现代计算机之父冯诺伊曼最先提出程序存储的思想,并成功将其运用在计算机的设计之中,该思想约定了用二进制进行计算和存储,还定义计算机基本结构为5个部分,分别是中央处理器(CPU)、内存、输入设备、输出设备、总线。

image

存储器:代码跟数据在RAM跟ROM中是线性存储,数据存储的单位是一个二进制位。最小的存储单位是字节。

总线:总线是用于CPU和内存以及其他设备之间的通信,总线主要有三种:

地址总线:用于指定CPU将要操作的内存地址。

数据总线:用于读写内存的数据。

控制总线:用于发送和接收信号,比如中断、设备复位等信号,CPU收到信号后响应,这时也需要控制总线。

输入/输出设备:输入设备向计算机输入数据,计算机经过计算后,把数据输出给输出设备。比如键盘按键时需要和CPU进行交互,这时就需要用到控制总线。

CPU:中央处理器,类比人脑,作为计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元。CPU用寄存器存储计算时所需数据,寄存器一般有三种:

通用寄存器:用来存放需要进行运算的数据,比如需进行加法运算的两个数据。

程序计数器:用来存储CPU要执行下一条指令所在的内存地址。

指令寄存器:用来存放程序计数器指向的指令本身。

在冯诺伊曼体系下电脑指令执行的过程:

CPU读取程序计数器获得指令内存地址,CPU控制单元操作地址总线从内存地址拿到数据,数据通过数据总线到达CPU被存入指令寄存器。

CPU分析指令寄存器中的指令,如果是计算类型的指令交给逻辑运算单元,如果是存储类型的指令交给控制单元执行。

CPU执行完指令后程序计数器的值通过自增指向下个指令,比如32位CPU会自增4。

自增后开始顺序执行下一条指令,不断循环执行直到程序结束。

CPU位宽:32位CPU一次可操作计算4个字节,64位CPU一次可操作计算8个字节,这个是硬件级别的。平常我们说的32位或64位操作系统指的是软件级别的,指的是程序中指令多少位。

线路位宽:CPU操作指令数据通过高低电压变化进行数据传输,传输时候可以串行传输,也可以并行传输,多少个并行等于多少个位宽。

1.2CPU简介

CentralProcessingUnit中央处理器,作为计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元。

CPU核心:一般一个CPU会有多个CPU核心,平常说的多核是指在一枚处理器中集成两个或多个完整的计算引擎。核跟CPU的关系是:核属于CPU的一部分。

寄存器:最靠近CPU对存储单元,32位CPU寄存器可存储4字节,64位寄存器可存储8字节。寄存器访问速度一般是半个CPU时钟周期,属于纳秒级别,

L1缓存:每个CPU核心都有,用来缓存数据跟指令,访问空间大小一般在32~KB,访问速度一般是2~4个CPU时钟周期。

cat/sys/devices/system/cpu/cpu0/cache/index0/size#L1数据缓存

cat/sys/devices/system/cpu/cpu0/cache/index1/size#L1指令缓存

L2缓存:每个CPU核心都有,访问空间大小在KB~2MB,访问速度一般是10~20个CPU时钟周期。

cat/sys/devices/system/cpu/cpu0/cache/index2/size#L2缓存容量大小

L3缓存:多个CPU核心共用,访问空间大小在2MB~64MB,访问速度一般是20~60个CPU时钟周期。

cat/sys/devices/system/cpu/cpu0/cache/index3/size#L3缓存容量大小

内存:多个CPU共用,现在一般是4G~G,访问速度一般是~个CPU时钟周期。

固体硬盘SSD:现在台式机主流都会配备,上述的寄存器、缓存、内存都是断电数据立马丢失的,而SSD里不会丢失,大小一般是G~1T,比内存慢10~倍。

机械盘HDD:很早以前流行的硬盘了,容量可在G~8T不等,访问速度比内存慢10W倍不等。

访问数据顺序:CPU在拿数据处理的时候几乎也是按照上面说得流程来操纵的,只有上面一层找不到才会找下一层。

CacheLine:CPU读取数据时会按照CacheLine方式把数据加载到缓存中,每个Cacheline=64KB,因为L1、L2是每个核独有到可能会触发伪共享,就是所以可能会将数据划分到不同到CacheLine中来避免伪共享,比如在JDK8新增加的LongAdder就涉及到此知识点。

:缓存系统中是以缓存行(cacheline)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。

JMM:数据经过种种分层会导致访问速度在不断提升,同时也带来了各种问题,多个CPU同时操作相同数据可能会造成各种BU个,需要加锁,这里在JUC并发已详细探讨过。

1.3CPU访问方式

CPU访问方式

内存数据映射到CPUCache是通过公式BlockN%CacheLineMax决定内存Block数据放到那个CPUCacheLine里。CPUCache主要有4部分组成。

CacheLineIndex:CPU缓存读取数据时不是按照字节来读取的,而是按照CacheLine方式存储跟读取数据的。

ValidBit:有效位标志符,值为0时表示无论CPULine中是否有数据,CPU都会直接访问内存,重新加载数据。

Tag:组标记,用来标记内存中不同BLock映射到相同CacheLine,用Tag来区分不同的内存Block。

Data:真实到内存数据信息。

CPU真实访问内存数据时只需要指定三个部分即可。

:要访问到CacheLine位置。

:表示用那个数据块。

Offset:CPU从CPUCache读取数据时不是直接读取CacheLine整个数据块,而是读取CPU所需的数据片段,称为Word。如何找到Word就需要个偏移量Offset。

1.4CPU访问速度

访问耗时对比

如上图所示,CPU访问速度是逐步变慢,所以CPU访问数据时需尽量在距离CPU近的高速缓存区访问,根据摩尔定律CPU访问速度每18个月就会翻倍,而内存的访问每18个月也就增长10%左右,导致的结果就是CPU跟内存访问性能差距逐步变大,那如何尽可能提高CPU缓存命中率呢?

数据缓存:遍历数据时候按照内存布局顺序访问,因为CPUCache是根据CacheLine批量操作数据的,所以你顺序读取数据会提速,道理跟磁盘顺序写一样。

指令缓存:尽可能的提供有规律的条件分支语句,让CPU的分支预测器发挥作用,进一步提高执行的效率,因为CPU是自带分支预测器,自动提前将可能需要的指令放到指令缓存区。

线程绑定到CPU:一个任务A在前一个时间片用CPU核心1运行,后一个时间片用CPU核心2运行,这样缓存L1、L2就浪费了。因此操作系统提供了将进程或者线程绑定到某一颗CPU上运行的能力。如Linux上提供了sched_setaffinity方法实现这一功能,其他操作系统也有类似功能的API可用。当多线程同时执行密集计算,且CPU缓存命中率很高时,如果将每个线程分别绑定在不同的CPU核心上,性能便会获得非常可观的提升。

1.5操作系统

计算机结构

有了冯诺伊曼计算机体系后,电脑想要为用户提供便捷的服务还需要安装个操作系统OperationSystem,操作系统是覆盖在硬件上的一层特殊软件,它管理计算机的硬件和软件资源,为其他应用程序提供大量服务。可以理解为操作系统是日常应用程序跟硬件之间的接口。日常你经常在用Windows/Linux系统,操作系统给我们提供了超级大的便利,但是你了解操作系统么?操作系统是如何进行内存管理、进程管理文件管理输入输出管理的呢?

2内存管理

你的电脑是32位操作系统,那可支持的最大内存就是4G,你有没有好奇为什么可以同时运行2个以上的2G内存的程序。应用程序不是直接使用的物理地址,操作系统为每个运行的进程分配了一套虚拟地址,每个进程都有自己的虚拟内存地址,进程是无法直接进行物理内存地址的访问的。至于虚拟地址跟物理地址的映射,进程是感知不到的!操作系统自身会提供一套机制将不同进程的虚拟地址和不同内存的物理地址进行映射。

虚拟内存

2.1MMU

MemoryManagementUnit内存管理单元是一种负责处理CPU内存访问请求的计算机硬件。它的功能包括虚拟地址到物理地址的转换、内存保护、中央处理器高速缓存的控制。现代CPU基本上都选择了使用MMU。

当进程持有虚拟内存地址的时候,CPU执行该进程时会操作虚拟内存,而MMU会自动的将虚拟内存的操作映射到物理内存上。

MMU

这里提一下,Java操作的时候你看到的地址是JVM地址,不是真正的物理地址。

2.2内存管理方式

操作系统主要采用内存分段和内存分页来管理虚拟地址与物理地址之间的关系,其中分段是很早前的方法了,现在大部分用的是分页,不过分页也不是完全的分页,是在分段的基础上再分页。

2.2.1内存分段

JVM内存模型

我们以上图的JVM内存模型举例,程序员会认为我们的代码是由代码段、数据段、栈段、堆段组成。不同的段是有不同的属性的,用户并不关心这些元素所在内存的位置,而分段就是支持这种用户视图的内存管理方案。逻辑地址空间是由一组段构成。每个段都有名称和长度。地址指定了段名称和段内偏移。因此用户段编号和段偏移来指定不同属性的地址。而虚拟内存地址跟物理内存地址中间是通过段表进行映射的,口说无凭,看图吧。

内存分段管理

如上虚拟地址有5个段,各段按如图所示来存储。每个段都在段表中有一个条目,它包括段在物理内存内的开始的基地址和该段的界限长度。例如段2为字节长,开始于位置4。因此对段2字节53的引用映射成位置4+53=。对段3字节的引用映射成位置3+=。

分段映射很简单,但是会导致内存碎片跟内存交互效率低。这里先普及下在内存管理中主要有内部内存碎片跟外部内存碎片。

内部碎片:已经被分配出去的的内存空间不经常使用,并且分配出去的内存空间大于请求所需的内存空间。

外部碎片:指可用空间还没有分配出去,但是可用空间由于大小太小而无法分配给申请空间的新进程的内存空间空闲块。

以上图为例,现在系统空闲是1++=2。那如果有个程序想要连续的使用0,内存分段模式下提供不了啊!上述三个是外部内存碎片。当然可以使用系统的Swap空间,先把段0写入到磁盘,然后再重新给段0分配空间。这样可以实现最终可用,可是但凡涉及到磁盘读写就会导致内存交互效率低。

swap空间利用

2.2.2内存分页

内存分页,整个虚拟内存和物理内存切成一段段固定尺寸的大小。每个固定大小的尺寸称之为页Page,在Linux系统中Page=4KB。然后虚拟内存跟物理内存之间通过页表来实现映射。

采用内存分页时内存的释放跟使用都是以页为单位的,也就不会产生内存碎片了。当空间还不够时根据操作系统调度算法,可能将最少用的内存页面swap-out换出到磁盘,用时候再swap-in换入,尽可能的减少磁盘刷写量,提高内存交互效率。

分页模式下虚拟地址主要由页号跟页内偏移量两部分组成。通过页号查询页表找到物理内存地址,然后再配合页内偏移量就找到了真正的物理内存地址。

分页内存寻址

32位操作系统环境下进程可操作的虚拟地址是4GB,假设一个虚拟页大小为4KB,那需要4GB/4KB=2^20个页信息。一行页表记录为4字节,2^20等价于4MB页表存储信息。这只是一个进程需要的,如果10个、个、个呢?仅仅是页表存储都占据超大内存了。

为了解决这个问题就需要用到多级页表,核心思想就是局部性分配。在32位的操作系统中将将4G空间分为行页目录项目(4KB),每个页目录项又对应行页表项。如下图所示:

32位系统二级分页

控制寄存器cr3中存放了页目录的物理地址,通过cr3寄存器可以找到页目录,而32位线性地址中的Directory部分决定页目录中的目录项,而页目录项中存放了要找的页表的物理基地址,再结合线性地址中的中间10位页表项,就可以找到页框的页表项。线性地址中的Offset部分占12位,因此页框的物理地址+线性地址Offset部分=页框中的任何一个字节。

分页后一级页就等价于4G虚拟地址空间,并且如果一级页表中那些地址没有就不需要再创建二级页表了!核心思想就是按需创建,当系统给每个进程分配4G空间,进程不可能占据全部内存的,如果一级目录页只有10%用到了,此时页表空间=一级页表4KB+0.1*4MB。这比单独的每个进程占据4M好用多了!

多层分页的弊端就是访问时间的增加

使用页表时读取内存中一页内容需要2次访问内存,访问页表项+并读取的一页数据。

使用二级页表的话需要三次访问,访问页目录项+访问页表项+访问并读取的一页数据。访存次数的增加也就意味着访问数据所花费的总时间增加。

而对于64位系统,二级分页就无法满足了,Linux从2.6.11开始采用四级分页模型。

PageGlobalDirectory全局页目录项

PageUpperDirectory上层页目录项

PageMiddleDirectory中间页目录项

PageTableEntry页表项

Offset偏移量。

64位寻址

2.2.2TLB

TranslationLookasideBuffer可翻译为地址转换后援缓冲器,简称为快表,属于CPU内部的一个模块,TLB是MMU的一部分,实质是cache,它所缓存的是最近使用的数据的页表项(虚拟地址到物理地址的映射)。他的出现是为了加快访问数据(内存)的速度,减少重复的页表查找。当然它不是必须要有的,但有它,速度就更快。

TLB

TLB很小,因此缓存的东西也不多。主要缓存最近使用的数据的数据映射。TLB结构如下图:

TLB查询

如果一个需要访问内存中的一个数据,给定这个数据的虚拟地址,查询TLB,发现有hit,直接得到物理地址,在内存根据物理地址取数据。如果TLB没有这个虚拟地址miss,那么只能费力地通过页表来查找了。日常CPU读取一个数据的流程如下:

CPU读取数据流程图

当进程地址空间进行了上下文切换时,比如现在是进程1运行,TLB中放的是进程1的相关数据的地址,突然切换到进程2,TLB中原有的数据不是进程2相关的,此时TLB刷新数据有两种办法。

全部刷新:很简单,但花销大,很多不必刷新的数据也进行刷新,增加了无畏的花销。

部分刷新:根据标志位,刷新需要刷新的数据,保留不需要刷新的数据。

2.2.3段页式管理

内存分段跟内存分页不是对立的,这俩可以组合起来在同一个系统中使用的,那么组合起来后通常称为段页式内存管理。段页式内存管理实现的方式:

先对数据不同划分出不同的段,也就是前面说的分段机制。

然后再把每一个段进行分页操作,也就是前面说的分页机制。

此时地址结构=段号+段内页号+页内位移。

每一个进程有一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号。

段页式管理

同时我们经常看到两个专业词逻辑地址跟线性地址。

逻辑

分享 转发
TOP
发新话题 回复该主题