操作系统学习笔记 ---- 硬件结构

1 冯诺伊曼模型

运算器、控制器、存储器、输入设备、输出设备
在这里插入图片描述
运算器、控制器是在中央处理器里的,存储器就我们常见的内存,输入输出设备则是计算机外接的设备,比如键盘就是输入设备,显示器就是输出设备。

存储单元和输入输出设备要与中央处理器打交道的话,离不开总线。关系图如下:

在这里插入图片描述

1.1 内存

内存存储的区域是线性的。在计算机数据存储中,存储数据的基本单位是字节(byte),1 字节等于 8 位(8 bit)。每一个字节都对应一个内存地址。

内存的地址是从 0 开始编号的,然后自增排列,最后一个地址为内存总字节数 - 1,内存的读写任何一个数据的速度都是一样的

1.2 中央处理器

CPU,32 位和 64 位 CPU 最主要区别在于一次能计算多少字节数据

32 位 CPU 一次可以计算 4 个字节;
64 位 CPU 一次可以计算 8 个字节;

32 位和 64 位,通常称为 CPU 的位宽,代表的是 CPU 一次可以计算(运算)的数据量。

CPU 内部还有一些组件,常见的有寄存器、控制单元和逻辑运算单元等。

控制单元负责控制 CPU 工作
逻辑运算单元负责计算
寄存器存储计算时的数据

常见的寄存器种类:

通用寄存器,用来存放需要进行运算的数据,比如需要进行加和运算的两个数据。
程序计数器,用来存储 CPU 要执行下一条指令「所在的内存地址」,注意不是存储了下一条要执行的指令,此时指令还在内存中,程序计数器只是存储了下一条指令「的地址」。
指令寄存器,用来存放当前正在执行的指令,也就是指令本身,指令被执行完成之前,指令都存储在这里。

1.3 总线

总线是用于 CPU 和内存以及其他设备之间的通信,总线可分为 3 种:

地址总线,用于指定 CPU 将要操作的内存地址;
数据总线,用于读写内存的数据;
控制总线,用于发送和接收信号,比如中断、设备复位等信号,CPU 收到信号后自然进行响应,这时也需要控制总线;

当 CPU 要读写内存数据的时候,1 地址总线->地址;2 控制总线->指令;3 数据总线->传输数据

1.4 输入、输出设备

输入设备向计算机输入数据,计算机经过计算后,把数据输出给输出设备。期间,如果输入设备是键盘,按下按键时是需要和 CPU 进行交互的,这时就需要用到控制总线了

2 程序执行的基本过程

在这里插入图片描述

那 CPU 执行程序的过程如下:

第一步,CPU 读取「程序计数器」的值,这个值是指令的内存地址,然后 CPU 的「控制单元」操作「地址总线」指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过「数据总线」将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存入到「指令寄存器」。

第二步,「程序计数器」的值自增,表示指向下一条指令。这个自增的大小,由 CPU 的位宽决定,比如 32 位的 CPU,指令是 4 个字节,需要 4 个内存地址存	放,因此「程序计数器」的值会自增 4;

第三步,CPU 分析「指令寄存器」中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给「逻辑运算单元」运算;如果是存储类型的指令,则交由「控制单元」执行;

一个程序执行的时候,CPU 会根据程序计数器里的内存地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。这个过程会不断循环,直到程序执行结束,这个不断循环的过程被称为 CPU 的指令周期

3 存储器

3.1 层次结构

3.1.1 CPU 内部的存储器的层次分布

寄存器

最靠近 CPU 的控制单元和逻辑计算单元的存储器。寄存器的数量通常在几十到几百之间,每个寄存器可以用来存储一定的字节(byte)的数据。

· 32 位 CPU 中大多数寄存器可以存储 4 个字节;
· 64 位 CPU 中大多数寄存器可以存储 8 个字节。

如果寄存器速度太慢,则会拉长指令处理周期。即,电脑反应很慢

寄存器的访问速度非常快,一般要求在半个 CPU 时钟周期内完成读写,CPU 时钟周期跟 CPU 主频息息相关,比如 2 GHz 主频的 CPU,那么它的时钟周期就是 1/2G,也就是 0.5ns(纳秒)。

CPU Cache

CPU Cache 用的是一种叫 SRAM(Static Random-Access Memory,静态随机存储器) 的芯片。

CPU 的高速缓存,通常可以分为 L1、L2、L3 这样的三层高速缓存,也称为一级缓存、二级缓存、三级缓存。
在这里插入图片描述

L1 高速缓存

L1 高速缓存的访问速度几乎和寄存器一样快,通常只需要 2~4 个时钟周期,而大小在几十 KB 到几百 KB 不等。
指令和数据在 L1 是分开存放的,所以 L1 高速缓存通常分成指令缓存数据缓存

L2 高速缓存

L2 高速缓存同样每个 CPU 核心都有, L2 高速缓存位置比 L1 高速缓存距离 CPU 核心 更远,它大小比 L1 高速缓存更大,CPU 型号不同大小也就不同,通常大小在几百 KB 到几 MB 不等,访问速度则更慢,速度在 10~20 个时钟周期。

L3 高速缓存

L3 高速缓存通常是多个 CPU 核心共用的,位置比 L2 高速缓存距离 CPU 核心 更远,大小也会更大些,通常大小在几 MB 到几十 MB 不等,具体值根据 CPU 型号而定。访问速度相对也比较慢一些,访问速度在 20~60个时钟周期。

** L1 Cache 和 L2 Cache 都是每个 CPU 核心独有的,而 L3 Cache 是多个 CPU 核心共享的。**

3.1.2 CPU 外部的存储器的层次分布

内存

使用的是一种叫作 DRAM (Dynamic Random Access Memory,动态随机存取存储器) 的芯片。

DRAM 存储一个 bit 数据,只需要一个晶体管和一个电容就能存储,但是因为数据会被存储在电容里,电容会不断漏电,所以需要「定时刷新」电容,才能保证数据不会被丢失,这是被称为动态存储器的原因。

DRAM 的数据访问电路和刷新电路都比 SRAM 更复杂,所以访问的速度会更慢,内存速度大概在 200~300 个 时钟周期之间

SSD/HDD 硬盘

SSD即,固体硬盘,结构和内存类似,但是它相比内存的优点是断电后数据还是存在的,而内存、寄存器、高速缓存断电后数据都会丢失。内存的读写速度比 SSD 大概快 10~1000 倍。

HDD即,机械硬盘(Hard Disk Drive, HDD),它是通过物理读写的方式来访问数据的,因此它访问速度是非常慢的,它的速度比内存慢 10W 倍左右。

3.2 层次关系

在这里插入图片描述
每个存储器只和相邻的一层存储器设备打交道,并且存储设备为了追求更快的速度,所需的材料成本必然也是更高,也正因为成本太高,所以 CPU 内部的寄存器、L1\L2\L3 Cache 只好用较小的容量,相反内存、硬盘则可用更大的容量

3.3 CPU Cache

数据结构

CPU Cache 是由很多个 Cache Line 组成的,Cache Line 是 CPU 从内存读取数据的基本单位,而 Cache Line 是由各种标志(Tag)+ 数据块(Data Block)组成:
在这里插入图片描述
CPU 在从 CPU Cache 读取数据的时候,并不是读取 CPU Cache Line 中的整个数据块,而是读取 CPU 所需要的一个数据片段,这样的数据统称为一个字(Word)

一个内存的访问地址,包括组标记CPU Cache Line 索引偏移量这三种信息,通过这些信息在 CPU Cache 中找到缓存的数据。而对于 CPU Cache 里的数据结构,则是由索引 + 有效位 + 组标记 + 数据块组成。

在这里插入图片描述
CPU访问一个内存地址的流程:

  • 先根据内存地址的索引信息,计算CPU Cache的索引,即对应Cache Line的地址
  • 判断Cache Line的有效位,有效则继续往下执行;无效则直接访问内存,重新加载数据
  • 对比内存地址的组标记和Cache Line的组标记,是则继续往下执行,不是则直接访问内存,重新加载数据
  • 根据内存地址的偏移量,从Cache Line的数据块中,读取对应的字(数据片段)

3.4 CPU 缓存一致性

针对写操作,在什么时机才把 Cache 中的数据写回到内存?

3.4.1 写直达

保持内存与 Cache 一致性最简单的方式是,把数据同时写入内存和 Cache 中,这种方法称为写直达(Write Through)。

写入前会先判断数据是否已经在 CPU Cache 里:

  • 如果数据已经在 Cache 里面,先将数据更新到 Cache 里面,再写入到内存里面;
  • 如果数据没有在 Cache 里面,就直接把数据更新到内存里面。

**问题:**无论数据在不在 Cache 里面,每次写操作都会写回到内存,写操作将花费大量时间

3.4.2 写回

在写回机制中,当发生写操作时,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Block「被替换」时才需要写到内存中,减少了数据写回内存的频率

具体流程:

  • 如果数据已经在CPU Cache里,则把数据更新在CPU Cache,同时该Cahce Block标记为脏(说明Cahce与内存的数据是不一致的)
  • 如果数据所对应的Cache Block存放的是 别的内存地址的数据,需要检查该Block是否为脏
    • 如果是脏,则将Block的数据写回内存,再把当前要写入的数据从内存读入到Block中,然后把当前写入的数据写入到Block里并标记为脏
    • 如果不是脏,把当前要写入的数据从内存读入到Block中,然后把当前写入的数据写入到Block里并标记为脏

总结: 数据写入Cache时,如果在缓存中命中,则直接写入Cache,并把数据对应的Cache Block标记为脏;如果未命中缓存,同时数据对应的Cache Block标记为脏的情况下,才将数据写入到内存。

3.5 缓存一致性问题

需同步不同核心里的缓存数据,实现该机制需要保证如下两点:

  • 第一点,某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为写传播(Write Propagation);
  • 第二点,某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为事务的串行化(Transaction Serialization)。

3.5.1 总线嗅探

当某一CPU核心修改自己独有的Cache中的变量,需要通过总线把该事件广播告知其它核心,其它核心监听总线上的广播并检查是否有相同的数据在自己独有的

问题:会加重总线的负载,无法保证事务串行化

3.5.2 MESI 协议

基于总线嗅探机制的 MESI 协议,可保障缓存一致性

  • Modified,已修改
  • Exclusive,独占
  • Shared,共享
  • Invalidated,已失效

通过该四个状态来标记 Cache Line 的状态

  • 「已修改」状态 即 脏标记,代表该 Cache Line 的数据已更新但没写入内存
  • 「已失效」状态,代表该 Cache Line 的数据已失效,不可读取该状态的数据
  • 「独占」和「共享」状态,都代表 Cache Line 的数据是干净的,此时 Cache Block 的数据与内存中的数据是一致的

「独占」和「共享」的差别:

独占状态的时候,数据只存储在一个 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有该数据。此时更新数据可直接自由写入,不存在一致性问题
 
在「独占」状态下的数据,如果有其他核心从内存读取了相同的数据到各自的 Cache ,那么这个时候,独占状态下的数据就会变成共享状态。

共享状态的数据不可直接更新,需要先向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后再更新当前 Cache 里面的数据。

在这里插入图片描述

在这里插入图片描述

3.6 读写数据 伪共享问题

多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象称为伪共享(False Sharing)

解决方法

在 Linux 内核中存在 __cacheline_aligned_in_smp 宏定义,是用于解决伪共享的问题。在这里插入图片描述

如果在多核(MP)系统里,该宏定义是 __cacheline_aligned,也就是 Cache Line 的大小;
而如果在单核系统里,该宏定义是空的;

举例:采用上面的宏定义使得变量在 Cache Line 里是对齐的。
在这里插入图片描述

Java 并发框架 Disruptor 使用「字节填充 + 继承」的方式,来避免伪共享的问题。
Disruptor 中有一个 RingBuffer 类会经常被多个线程使用,代码如下:
在这里插入图片描述
CPU Cache 从内存读取数据的单位是 CPU Cache Line,一般 64 位 CPU 的 CPU Cache Line 的大小是 64 个字节,一个 long 类型的数据是 8 个字节,所以 CPU 一下会加载 8 个 long 类型的数据。

根据 JVM 对象继承关系中父类成员和子类成员,内存地址是连续排列布局的,因此 RingBufferPad 中的 7 个 long 类型数据作为 Cache Line 前置填充,而 RingBuffer 中的 7 个 long 类型数据则作为 Cache Line 后置填充,这 14 个 long 变量没有任何实际用途,更不会对它们进行读写操作。
在这里插入图片描述
另外,RingBufferFelds 里面定义的这些变量都是 final 修饰的,意味着第一次加载之后不会再修改, 又由于「前后」各填充了 7 个不会被读写的 long 类型变量,所以无论怎么加载 Cache Line,这整个 Cache Line 里都没有会发生更新操作的数据,于是只要数据被频繁地读取访问,就自然没有数据被换出 Cache 的可能,也因此不会产生伪共享的问题。

3.7 CPU如何选择线程

在 Linux 内核中,进程和线程都是用 task_struct 结构体表示的,区别在于线程的 task_struct 结构体里部分资源是共享了进程已创建的资源,比如内存地址空间、代码段、文件描述符等。

一般来说,没有创建线程的进程,是只有单个执行流,它被称为是主线程。
不管是主线程、普通线程,对应到内核里都是 task_struct
在这里插入图片描述
Linux 内核里的调度器,调度的对象就是 task_struct,统称为任务。

在 Linux 系统中,根据任务的优先级以及响应要求,主要分为两种,其中优先级的数值越小,优先级越高:

  • 实时任务,对系统的响应时间要求很高,也就是要尽可能快的执行实时任务,优先级在 0~99 范围内的就算实时任务;
  • 普通任务,响应时间没有很高的要求,优先级在 100~139 范围内都是普通任务级别;

调度类

在这里插入图片描述

Deadline 和 Realtime 这两个调度类,都是应用于实时任务的,这两个调度类的调度策略合起来共有这三种,它们的作用如下:

  • SCHED_DEADLINE:是按照 deadline 进行调度的,距离当前时间点最近的 deadline 的任务会被优先调度;
  • SCHED_FIFO:对于相同优先级的任务,按先来先服务的原则,但是优先级更高的任务,可以抢占低优先级的任务,也就是优先级高的可以「插队」;
  • SCHED_RR:对于相同优先级的任务,轮流着运行,每个任务都有一定的时间片,当用完时间片的任务会被放到队列尾部,以保证相同优先级任务的公平性,但是高优先级的任务依然可以抢占低优先级的任务;

而 Fair 调度类是应用于普通任务,都是由 CFS 调度器管理的,分为两种调度策略:

  • SCHED_NORMAL:普通任务使用的调度策略;
  • SCHED_BATCH:后台任务的调度策略,不和终端进行交互,因此在不影响其他需要交互的任务,可以适当降低它的优先级。

中断
为了避免由于中断处理程序执行时间过长,而影响正常进程的调度,Linux 将中断处理程序分为上半部和下半部:

  • 上半部,对应硬中断,由硬件触发中断,用来快速处理中断;
  • 下半部,对应软中断,由内核触发中断,用来异步处理上半部未完成的工作;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Swing_zzZ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值