Review操作系统-CSDN博客

120 阅读15分钟
  1. 操作系统由什么组成?
  2. 什么是阻塞IO和非阻塞IO?
  3. 用户态到内核态怎么切换?中断的原因?
  4. 进程间的通信方式?同一个机器之间的进程通信哪种最有效?
  5. select epllo?2
  6. 为什么要分页?你对操作系统内存页有什么了解?
  7. 为什么要有虚拟内存?有什么用?
  8. 操作系统怎么从虚拟内存得到实际物理内存?
  9. 虚拟地址和物理地址如何映射?
  10. 进程和线程的区别?
  11. 进程间的调度算法?
  12. 软中断?
  13. CPU三级缓存?寄存器?
  14.  IO模型?网络模型?零拷贝?
  • 操作系统属于软件,但是它是内核态。
  • fd:File Description - 文件描述符。
  • 比如我们现在有一台Linux服务器,我们把它作为网络服务器来使用,当其它人连接到我们的服务器上的时候,其实连接的表现形式就是一个文件描述符,我们通过文件描述符来判断具体是哪个用户的连接。
  • 一次IO本质上是一个read()和write()的系统调用。

什么是内核?

  • 只有内核程序访问的内存空间我们称之为内核,内核是操作系统的核心组件,提供了操作系统最核心的能力。
  • 微内核只保留最基本的能力,比如进程调度、中断等等
  • 大内核的特征就是系统内核的所有模块,比如进程调度、内存管理、文件系统、设备驱动等等,都运行在内核态。

什么是系统调用?

  • 系统给应用程序提供的唯一接口,可获得OS操作系统的服务,在用户态发生,核心态处理。
  • 我们运行的用户程序中,凡是与系统级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用的方式向操作系统发出服务请求,并由操作系统代为完成。

​​​​​​​RAM和ROM的区别?

  • RAM为随机存储,掉电后不会保存数据;而ROM可以在掉电的情况下,依然保存原有的数据。

​​​​​​​​​​​​​​什么是进程和进程表?

  • 进程就是正在执行程序的实例。
  • 为了跟踪每个进程的活动状态,维护了一个进程表(寄存器、程序计数器、进程ID、父进程...)。

存储器的架构 / 存储器的层次结构:

常见的业务流程来引出:

MySQL(磁盘) - List(内存) - 逻辑运算(CPU) - List(内存) - MySQL(磁盘)

  • 我们都知道MySQL它是一个磁盘数据库,也就是说它的数据其实都能再我们的电脑中找到一个对应的文件来去表示,它其实可以是一个.ibd文件,所以说MySQL对应的存储设备就是磁盘。
  • 数据从磁盘中读取出来之后,其实是读取到内存当中,内存跟我们整体的性能也是息息相关的,如果内存大,那我们一次性加载的数据就更多,我们都加载过去之后,去处理的时候也会更方便,所以内存跟我们整体的性能也是有关系的,比如我们在做一些JVM调优的时候,很大一部分我们要调的就是一个虚拟机的大小,虚拟机时运行在我们内存中的,所以我们虚拟机的大小其实跟内存也是有关系的。
  • 将数据读到内存中之后,接下来我们可能要做一些逻辑处理,比如算术运算等,这些逻辑处理的操作其实就是一个又一个的指令,操作系统用来执行指令的单元其实就是CPU。
  • CPU - 中央处理器,它不仅时一个执行指令的单元,同时它也是一个小型的存储结构,CPU的组成:CPU寄存器 => 数据寄存器、指令寄存器、地址寄存器(比如我们有些数据在内存中,要找到该数据,就首先要知道数据在内存中的地址、PC寄存器/程序计数器(跟JVM中的程序计数器类似,用于记录你下一条要执行的指令的地址)  +  逻辑运算单元ALU(主要是加法器和乘法器,为什么没有减法器,因为减法的话它其实就可以通过补码加法的方式来实现)     +   CPU三级缓存架构    +    时钟(用来控制一些调度算法,比如说轮询,A进程用2s,B进程用2s,然后重置,就是一个时间单元)。
  • 同理总线也分为很多,比如数据总线(传输数据的)、地址总线(传输地址的)和指令总线(传输指令的)。总线就是用来传输数据的,数据会从总线上以电信号的形式进行传递。

  • 服务器 => 磁盘 => 内存 => CPU寄存器:CPU的读写速度是最快的,然后其次是内存,接下来是磁盘,再其次就是远程的一些数据库了,因为我们如果从远程的数据库上读东西的话,我们是要走一个网络请求的,我们要先发送一个网络请求,就是经过网络发过去,然后再返回来,相比本地的话,它要多一个网络传输的过程。
  • 我们都知道我们经常在业务当中会聊到缓存 - Redis,之所以用Redis是因为大部分场景下MySQL太慢了 - MYSQL是磁盘数据库,磁盘IO导致性能缓慢,而Redis是内存数据库,内存的性能要比磁盘高大约十倍
  • 不仅内存跟磁盘之间的速度差异比较大,我们CPU跟内存之间的速度差异也非常的大,因为CPU它不仅处理指令的速度特别快,并且它处理完成之后就立刻要把这个处理完的结果发送出去,所以说它的速度特别快;而在内存里面,我们的数据可能要存储很久,并且我们内存比较大,可能有很多个部分,那去找数据的时候可能还需要一部分时间,所以内存相比CPU要慢很多,CPU的速度:内存的速度 = 100 ; 1
  • 内存为了能够匹配CPU的速度,因此在内存跟CPU之间也是有一个缓存的机制的,也就是CPU的三级缓存架构。
  • CPU三级缓存的出现是为了弥补CPU跟内存之间的读写数据的速度差异,通过三级缓存来提高我们对数据的处理速度。

CPU的工作原理:

  • 我们知道电脑磁盘上的可执行文件(比如.exe文件),它具体的内容其实就是一个二进制的文本文件,你看不懂,我也看不懂,但是CPU说它能看懂,在它眼里,这些二进制代表着指令和数据。
  • 当我们点击了可执行文件,可执行文件会运行起来变成一个进程实例,然后就会和CPU不断地产生数据交互,CPU寄存器会不断地处理和反馈这些数据。
  • 我们知道磁盘读写相对比较慢,通常只用于永久性数据地存储,而真正运行的进程数据都是完全放到内存里的,所以真实情况应该是CPU和内存频繁的进行数据交互。

缓存设计

  • 缓存是一个非常强大的存在,在计算机世界无处不在。

  • 关于缓存,我们必须清楚这三个事实:

  1. 首先,缓存存在的意义就是解决速度不同步的问题,不同的设备,读写速度、处理能力都是不同的,当处理任务不同步的时候,系统的整体效率就会很低,缓存就是为了解决这类问题应运而生的。
  2. 缓存是最常见的优化手段之一,例如,电商的促销活动,如何应对海量的并发和交易量,其实只要在每一层都应用好缓存和负载均衡技术即可解决大部分问题。
  3. 最后一点,缓存的设计是否成功,命中率是最重要的指标,如果命中率过低,意味着缓存并没有起到应有的作用。

来看一下CPU中缓存的工作流程:

  • 我们知道CPU要从内存中读取数据,但是这种操作往往是很频繁的,甚至很多数据是要反复的被读写到寄存器中,现在通常的做法就是为CPU设置三个层级的缓存,也就是图中的L1、L2、L3,越靠近CPU的层级,速度越快,容量越小。

CPU中三级缓存的读写工作流程:

  • 读场景:优先读缓存,CPU先找L1,如果未能命中,去找L2,依次找下去,最终有可能到内存,需要把热点数据尽可能保留在更快的缓存中,提升整体效率。

  • 写场景,有两种方式:

  1. 方式一:比如我们缓存中的数据要更新,那就写缓存,写缓存的同时顺便把内存也写了,这是第一种比较常见的方式,这种方式整体执行来说相对比较安全,因为你写缓存的时候顺便把内存也写了,缓存与内存是同时写的,这样子就避免了缓存数据不一致的问题。它的缺点就是慢,还有就是没必要,因为很有可能你的缓存写完之后,你下次读还是在缓存中读,然后又发生了修改,然后继续写缓存,整个过程可能完全就没有对内存中的数据做任何的读取,这时候如果你写内存的话,你写了但是你不读内存,其实这样子就没有必要,所以这就是这种方式的缺点:慢、没必要。
  2. 方式二:我们如果要写数据,我们只写缓存,如果缓存当中的数据发生了修改,这时候我们才去考虑写内存,也就是说我们写内存的操作并不是跟写缓存同时发生的,而是只有当缓存中的数据发生了修改,我们才会进行一个写内存。这种方式优点就很明显,我们每次写操作只写缓存,相对于来说速度会快很多,但是也有一个缺点,如果是多进程操作的场景,对于CPU来说是一个多进程的概念,如果我们多进程同时去操作缓存中的一些数据,我们如何确保这个数据已经更新了,也就是数据一致性的问题,比如:进程A写完缓存,但还没有写内存,然后进程B去读了,这时候进程B读的就是一个脏数据,也就是说多进程下可能会出现一个并发问题,也就是缓存跟内存数据不一致的问题。
如何解决这个并发问题呢?也就是缓存跟内存数据不一致的问题:
  • 有两种解决方案:

  1. 第一种方案 - 写传播: 我们多进程对应的就是CPU的多核,因此进程对应的就是CPU的核数。写传播就是比如说:我们的CPU核A,如果CPU核A写缓存的时候对缓存进行了一个修改,此时它会通过总线进行一个消息传播,它会把CPU核A或者说叫进程A写缓存这个消息,通过总线传播给其它的CPU核或所有的进程,比如传播给核 / 进程B、C、D。

  2. 第二种方案 - 类似于一个串行化的策略,对应的就是一个协议 - MESI协议(缓存一致性协议),: 串行化的策略可以理解为我们这个MySQL的串行化,它们是一个思想,也就是说我们要确保多个进程之间操作的顺序性,也就是我的核A修改完之后,进程 / 核B才能修改,然后进程 / 核C才能修改,B的读取一定要在A写完之后,C的读取一定要在B写完之后,也就是说它相当于把我们的进程给串行化了。

  • 以上就是常见的CPU解决这种数据不一致的两种方案。

现在的CPU由于大部分是多核架构,所以缓存大致上是这样的结构:

  • 可以理解成L1是为寄存器服务的,所以离寄存器最近,一定是每个核心独占的,而L3是为内存服务的,所以一定是多核共享的。独享的缓存浪费空间,但是没有多个核心同时争抢的情况,所以效率更高,共享的缓存由于存在多核争抢,通常需要锁或者串行化来解决,效率不如独享。
我们知道缓存是分三级的,为什么大家都只讨论第三级?
  • 通常L1缓存特别小,而L2的容量也不大,由于L1和L2本身太小,所以往往厂商已经不做宣传。

CPU三级缓存的场景题目 - 伪共享

什么是伪共享?

  • 首先伪共享是CPU缓存中的一个场景,我们知道通过CPU的三级缓存架构可以来提高我们对数据的处理速度,伪共享问题其实就是说:多线程同时访问相邻内存地址上的数据,而这些数据可能存储在同一个Cache Line缓存行中,由于缓存一致性协议设计到对缓存行的读取、写入和更新,这时不同线程之间的访问修改可能会引发缓存行的失效,从而降低并发访问性能。也就是说这个数据在Cache Line缓存行中,但是我们的读取并没有用到Cache缓存,而是走了一个内存,也就是说速度没有做任何提升, 速度不变,这就是缓存伪共享的一个概念。
  • 虽然我们每次读写都用上了Cache缓存,但是我们的速度并没有提升,速度不变,这就是一个伪共享。
为什么会出现(缓存)伪共享这种问题呢?
  • 首先我们的CPU三级缓存,是以一个缓存行为单元的Cache Line。
  • 比如我们每次读写数据,并不是说就像我们在Java当中读数组中的数据一样,一个下标一个下标的读,而是它会直接读一块儿出来,这一块儿的话就称为一个Cache Line,叫做一个缓存行。
  • 一个缓存行它其实是有大小的,它里面会存很多数据,比如说我们读缓存很有可能一次会读到多条数据,然后这多条数据里面会包含我们需要的那条数据。
  • 假设此时有这样一个场景,我们有两个进程或两个线程,线程A先去读x,然后把x从内存中读到一个Cache Line缓存行中,线程B做了一个类似的动作,它把y从内存中读到一个Cache Line缓存行中,比如现在数据x和数据y都在同一个Cache Line缓存行中,此时线程A如果说去修改x,也就意味着整个缓存行就发生了一个变动,这个变动是要通知到其它线程的,就比如说它要通知给线程B,线程B收到通知之后,它会把自己数据所在的这个Cache Line缓存行设置为失效,因为该缓存行当中的数据已经发生了一个变动/修改,所以说它这个缓存里面的数据跟内存里面的数据很有可能是不一致的,这时候,线程B为了确保数据的一致性,它就不能从缓存中读了,因为它要把这个缓存行Cache Line设置为失效,此时线程B再去读数据y的话,就要从内存中去读了,因为数据y所在的缓存行已经被线程B设置为已失效,也就是说:数据y在缓存行中,并且线程B没有去动它,但是线程B对数据y每次的读取还是要在内存中读,此时就出现一个缓存伪共享的情况 => 虽然说这个数据在Cache Line缓存行中,但是我们的读取并没有用到Cache缓存,而是走了一个内存,也就是说速度没有做任何提升,这就是缓存伪共享的一个概念。
如何解决缓存伪共享这种问题呢?
  • 我们Java中有一个概念叫做对象头,里面会存mark word这些东西。
  • 对象头里面有一部分字段叫做对象填充或者叫做对齐填充,这一段其实就是一串没有意义的全填0或者全填1的一部分数据,它没有任何的业务意义或者说实际意义,它的作用就是为了确保把对象的大小补齐至8Byte的整数倍,
为什么是8Byte的倍数呢?
  • 跟字长相关,什么是字长呢?
  • 字长指的是CPU一次能够处理的字节数,通常是CPU寄存器的宽度,一般来说在64位的计算机体系结构中,字长是8字节(64位)所以说Java对象头中它会把对象填充至8Byte的倍数这样可以保证对象在读取或写入时能够始终按照字节大小对齐,提高内存访问效率,而在32位体系结构中,字长是4字节(32位)。