秋招复习笔记——八股文部分:操作系统

笔试得刷算法题,那面试就离不开八股文,所以特地对着小林coding的图解八股文系列记一下笔记。

这一篇笔记是图解系统的内容。

硬件结构

CPU执行程序

计算机基本结构为 5 个部分,分别是运算器、控制器、存储器、输入设备、输出设备,这 5 个部分也被称为冯诺依曼模型

存储单元和输入输出设备的通讯如下:

通过总线完成通讯

内存地址从0编号,自增排列。

CPU一次计算的数据量分为32位(4个字节)以及64位(8个字节),CPU中有寄存器来存储计算时数据,有通用寄存器、程序计数器以及指令寄存器。

总线有地址总线、数据总线以及控制总线,分为完成寻址、读写内存数据以及发送和接收信号。

CPU位宽一般不小于线路位宽,32位CPU一般为32位地址总线。

程序执行的时候,耗费的 CPU 时间少就说明程序是快的,对于程序的 CPU 执行时间,可以拆解成 CPU 时钟周期数(CPU Cycles)和时钟周期时间(Clock Cycle Time)的乘积

CPU执行时间

CPU 时钟周期数可以进一步拆解成:「指令数 x 每条指令的平均时钟周期数(Cycles Per Instruction,简称 CPI)」,由此继续化简公式:

CPU执行时间

磁盘与内存

CPU中有数据存储,也就是CPU Cache,成为CPU高速缓存,通常分为L1、L2、L3三成,L1 Cache通常分为“数据缓存”和“指令缓存”。

CPU中,操作最快的是寄存器,然后依次是CPU Cache,然后是内存,最后是硬盘。

CPU Cache 用的是一种叫 SRAM(Static Random-Access Memory,静态随机存储器) 的芯片。而三层Cache如下所示:

CPU Cache

L1 高速缓存的访问速度几乎和寄存器一样快,通常只需要 2~4 个时钟周期,而大小在几十 KB 到几百 KB 不等。每个CPU核心都有,且会分成指令缓存和数据缓存

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

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

内存用的芯片和 CPU Cache 有所不同,它使用的是一种叫作 DRAM (Dynamic Random Access Memory,动态随机存取存储器) 的芯片。访问速度更慢,内存速度大概在 200~300 个 时钟周期之间。

CPU访问内存中数据的流程:

CPU访问内存中数据

提升CPU中代码运行效率

CPU Cache 的数据是从内存中读取过来的,它是以一小块一小块读取数据的,而不是按照单个数组元素来读取数据的,在 CPU Cache 中的,这样一小块一小块的数据,称为 Cache Line(缓存块)。Cache Line 是 CPU 从内存读取数据的基本单位,而 Cache Line 是由各种标志(Tag)+ 数据块(Data Block)组成。

一般CPU访问内存,就会通过直接映射Cache来完成(Direct Mapped Cache)。一个内存的访问地址,包括组标记、CPU Cache Line 索引、偏移量这三种信息,于是 CPU 就能通过这些信息,在 CPU Cache 中找到缓存的数据。而对于 CPU Cache 里的数据结构,则是由索引 + 有效位 + 组标记 + 数据块组成

内存地址的访问

如果 CPU 所要操作的数据在 CPU Cache 中的话,这样将会带来很大的性能提升。访问的数据在 CPU Cache 中的话,意味着缓存命中,缓存命中率越高的话,代码的性能就会越好,CPU 也就跑的越快

按照内存布局顺序访问,将可以有效的利用 CPU Cache 带来的好处,这样我们代码的性能就会得到很大的提升,提升数据缓存的命中率。

现代 CPU 都是多核心的,线程可能在不同 CPU 核心来回切换执行,这对 CPU Cache 不是有利的,虽然 L3 Cache 是多核心之间共享的,但是 L1 和 L2 Cache 都是每个核心独有的,如果一个线程在不同核心来回切换,各个核心的缓存命中率就会受到影响,相反如果线程都在同一个核心上执行,那么其数据的 L1 和 L2 Cache 的缓存命中率可以得到有效提高,缓存命中率高就意味着 CPU 可以减少访问内存的频率。所以可以把线程绑定到某个CPU核心来提升多核CPU的缓存命中率。

CPU的缓存一致性

把CPU的Cache数据写回到内存的时机很重要。

为了要减少数据写回内存的频率,就出现了写回(Write Back)的方法。
当发生写操作时,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Block「被替换」时才需要写到内存中,减少了数据写回内存的频率,这样便可以提高系统的性能。

写回机制流程图

由于CPU当前大多是多核的,而L1和L2都是多个核心独有的,就会有缓存一致性问题(Cache Coherence)。

写传播的原则就是当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心。最常见实现的方式是总线嗅探(Bus Snooping)。但是这就需要 CPU 时刻监听总线,负担很重。

MESI协议则通过添加状态机来改善总线嗅探的问题。具体的方法可以在面试前直接看下面的表格熟悉一下

MESI的操作

CPU执行任务

多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象称为伪共享(False Sharing)。在 Linux 内核中存在 __cacheline_aligned_in_smp 宏定义,其可以在原先a、b两个在物理内存地址上连续的变量,通过这个宏定义将b的地址设置为 Cache Line 对齐地址,避免伪共享。

CPU选择执行的线程,在 Linux 中,线程和进程都是用 task_struct 结构体表示,调度器调度这个结构体,来完成执行。这些任务分为普通任务和实时任务。优先级数值越小则优先级越高。

对于普通任务,一般用完全公平调度(Completely Fair Scheduling)算法,优先选择虚拟运行时间 vruntime 少的任务来执行,当然还要考虑普通任务的权重值,nice 级别越低权重值越大。nice 的值能设置的范围是 -20~19, 值越低,表明优先级越高,因此 -20 是最高优先级,19 则是最低优先级,默认优先级是 0。

vruntime计算公式

软中断

Linux 系统为了解决中断处理程序执行过长和中断丢失的问题,将中断过程分成了两个阶段,分别是「上半部和下半部分」。

  • 上半部用来快速处理中断,一般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。
  • 下半部用来延迟处理上半部未完成的工作,一般以「内核线程」的方式运行。

中断处理程序的上部分和下半部可以理解为:

  • 上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行;
  • 下半部是由内核触发,也就说软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行;

数据存储

计算机中,负数通过补码来完成保存。所谓的补码就是把正数的二进制全部取反再加 1。用了补码的表示方式,对于负数的加减法操作,实际上是和正数加减法操作一样的。

存小数的方法可以直接去看这个链接:小林图解系统中的数据存储部分

内存管理

虚拟内存

操作系统引入了虚拟内存,进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存。

为了减少传统的分段管理导致的内存碎片,提出了内存分页(Paging)方法,把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为 4KB

虚拟地址和物理地址由MMU通过页表映射

内存管理单元(MMU)会负责映射工作,采用了分页,页与页之间是紧密排列的,所以不会有外部碎片。

如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。一旦需要的时候,再加载进来,称为换入(Swap In)。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。

Swap操作提升内存交换效率

在分页机制下,虚拟地址分为两部分页号页内偏移页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址。但这种简单的分页,存在空间缺陷,因为页表由于多个进程会变得非常庞大,故采用多级页表(Multi-Level Page Table)来解决

例如在32位 CPU 下,页大小 4KB ,一个进程的页表需要 4 MB 空间,而对其进行二次分页,将页表(一级页表)分为 1024 个页表(二级页表),每个表(二级页表)中包含 1024 个「页表项」,形成二级分页。这样,如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表

二级分页

但是多级页表,会造成虚拟内存到物理地址上更多的转换步骤,所以把最常访问的几个页表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为页表缓存、转址旁路缓存、快表等

内存分段和内存分页并不是对立的,它们是可以组合起来在同一个系统中使用的,那么组合起来后,通常称为段页式内存管理。

Linux 系统中的每个段都是从 0 地址开始的整个 4GB 虚拟空间(32 位环境下),也就是所有的段的起始地址都是一样的。这意味着,Linux 系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护

在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分, 32 位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间。

内核空间与用户空间的区别:

  • 进程在用户态时,只能访问用户空间内存;
  • 只有进入内核态后,才可以访问内核空间的内存;

其中用户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。

虚拟内存作用:

  • 第一,虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
  • 第二,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
  • 第三,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。

malloc分配内存

malloc 申请内存的时候,会有两种方式向操作系统申请堆内存。

  • 方式一:通过 brk() 系统调用从堆分配内存
  • 方式二:通过 mmap() 系统调用在文件映射区域分配内存;

方式一实现的方式很简单,就是通过 brk() 函数将「堆顶」指针向高地址移动,获得新的内存空间。方式二通过 mmap() 系统调用中「私有匿名映射」的方式,在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存。这两个方法切换的阈值是 128KB, 小于 128KB 就会采用 brk() 来分配。

malloc会分配的时虚拟内存,且会预分配更大的空间作为内存池。free 内存后堆内存还存在,是针对 malloc 通过 brk() 方式申请的内存的情况。如果 malloc 通过 mmap 方式申请的内存,free 释放内存后就会归归还给操作系统。

如果都用 mmap 来分配内存,等于每次都要执行系统调用。执行系统调用会需要切换进入内核态,然后回到用户态,需要消耗时间。同时,mmap 分配内存释放会归还给操作系统,所以分配的地址都会缺页,第一次访问该虚拟地址就会触发缺页中断,而 brk() 申请的堆空间连续,再次申请只需要从内存池取出;但如果都用 brk(),会造成堆内大量不可用碎片,导致内存泄漏。

缺页中断出发后,进程从用户态进入内核态,缺页中断函数会查看是否有空余的物理内存,有的话就分配并建立映射关系。

内存满了会发生什么

缺页中断触发,如果没有空闲物理内存,就会进行回收内存

  • 后台内存回收(kswapd):在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。
  • 直接内存回收(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。

如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会放最后的大招了 ——触发 OOM (Out of Memory)机制。OOM Killer 机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer 会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。

申请物理内存流程

主要有两类内存可以被回收,而且它们的回收方式也不同。

  • 文件页(File-backed Page):内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)都叫作文件页。大部分文件页,都可以直接释放内存,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。所以,回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存
  • 匿名页(Anonymous Page):这部分内存没有实际载体,不像文件缓存有硬盘文件这样一个载体,比如堆、栈数据等。这部分内存很可能还要再次被访问,所以不能直接释放内存,它们回收的方式是通过 Linux 的 Swap 机制,Swap 会把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了

从文件页和匿名页的回收操作来看,文件页的回收操作对系统的影响相比匿名页的回收操作会少一点,因为文件页对于干净页回收是不会发生磁盘 I/O 的,而匿名页的 Swap 换入换出这两个操作都会发生磁盘 I/O。所以尽可能先回收文件页。

尽早触发 kswapd 内核线程异步回收内存。具体看这里:kswapd 回收设置

  • 在 32 位操作系统,因为进程理论上最大能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败。
  • 在 64位 位操作系统,因为进程理论上最大能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区:
    • 如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出);
    • 如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行;

如何避免预读失效和缓存污染

LInux 中,读取的文件数据缓存在文件系统中的 Page Cache,大小有限,所以希望通过保留频繁访问的数据在其中,经典的是采用 LRU(Least recently used)算法完成,一般采用链表,头部是最近使用的数据,尾部就是最久没被使用的。

预读机制会把多个 Page 数据装入 Page Cache,以此减少磁盘 I/O 次数,提高吞吐量。但如果这些被提前加载进来的页,并没有被访问,相当于这个预读工作是白做了,这个就是预读失效。为了避免此类问题,最好就是让预读页停留在内存里的时间要尽可能的短,让真正被访问的页才移动到 LRU 链表的头部,从而保证真正被读取的热数据留在内存里的时间尽可能长。Linux 操作系统实现两个了 LRU 链表:活跃 LRU 链表(active_list)和非活跃 LRU 链表(inactive_list);将数据分为了冷数据和热数据,然后分别进行 LRU 算法。预读页就只需要加入到 inactive list 区域的头部,当页被真正访问的时候,才将页插入 active list 的头部

缓存污染:当我们在批量读取数据的时候,由于数据被访问了一次,这些大量数据都会被加入到「活跃 LRU 链表」里,然后之前缓存在活跃 LRU 链表(或者 young 区域)里的热点数据全部都被淘汰了,如果这些大量的数据在很长一段时间都不会被访问的话,那么整个活跃 LRU 链表(或者 young 区域)就被污染了。为了解决缓存污染,只要我们提高进入到活跃 LRU 链表(或者 young 区域)的门槛,就能有效地保证活跃 LRU 链表(或者 young 区域)里的热点数据不会被轻易替换掉。在 Linux 中,只有内存页被访问第二次,才将页从 inactive list 升级到 active list 中。

Linux 虚拟内存管理、物理内存管理

直接看这里:Linux 虚拟内存

进程管理

基础知识

进程

编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当我们运行这个可执行文件后,它会被装载到内存中,接着 CPU 会执行程序中的每一条指令,那么这个运行中的程序,就被称为「进程」(Process)

单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生并行的错觉,实际上这是并发。

在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态

进程的三种基本状态

再加上创建状态和结束状态,完整的进程状态图如下:

进程状态图

同时,阻塞装填太多,进程可能占用大量物理内存空间,可以把这些空间换出到硬盘,也就是 swap out,那么需要一个挂起状态来描述没有占用实际物理内存空间的情况。同时挂起还需分成阻塞挂起和就绪挂起。那么就变成了下图:

进程状态切换

PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。包含了进程描述信息、进程控制和管理信息、资源分配清单以及 CPU 相关信息。

PCB 一般通过链表形式组织,把具有相同状态的进程链在一起,组成各种队列

01 创建进程

操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源。

创建进程的过程如下:

  • 申请一个空白的 PCB,并向 PCB 中填写一些控制和管理进程的信息,比如进程的唯一标识等;
  • 为该进程分配运行时所必需的资源,比如内存资源;
  • 将 PCB 插入到就绪队列,等待被调度运行;

02 终止进程

进程可以有 3 种终止方式:正常结束、异常结束以及外界干预(信号 kill 掉)。

当子进程被终止时,其在父进程处继承的资源应当还给父进程。而当父进程被终止时,该父进程的子进程就变为孤儿进程,会被 1 号进程收养,并由 1 号进程对它们完成状态收集工作。

终止进程的过程如下:

  • 查找需要终止的进程的 PCB;
  • 如果处于执行状态,则立即终止该进程的执行,然后将 CPU 资源分配给其他进程;
  • 如果其还有子进程,则应将该进程的子进程交给 1 号进程接管;
  • 将该进程所拥有的全部资源都归还给操作系统;
  • 将其从 PCB 所在队列中删除;

03 阻塞进程

当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待,它只能由另一个进程唤醒。

阻塞进程的过程如下:

  • 找到将要被阻塞进程标识号对应的 PCB;
  • 如果该进程为运行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值