操作系统八股文知识点汇总

1. 程序编译过程

在这里插入图片描述

  • gcc HelloWorld.c -E -o HelloWorld.i 预处理:加入头文件,替换宏。
  • gcc HelloWorld.c -S -c -o HelloWorld.s 编译:包含预处理,将 C 程序转换成汇编程序。
  • gcc HelloWorld.c -c -o HelloWorld.o 汇编:包含预处理和编译,将汇编程序转换成可链接的二进制程序。
  • gcc HelloWorld.c -o HelloWorld 链接:包含以上所有操作,将可链接的二进制程序和其它别的库链接在一起,形成可执行的程序文件。

2. 内核结构与设计

计算机资源

  1. 总线,负责连接各种其它设备,是其它设备工作的基础。
  2. CPU,即中央处理器,负责执行程序和处理数据运算。
  3. 内存,负责储存运行时的代码和数据。
  4. 硬盘,负责长久储存用户文件数据。
  5. 网卡,负责计算机与计算机之间的通信。
  6. 显卡,负责显示工作。
  7. 各种 I/O 设备,如显示器,打印机,键盘,鼠标等。
    在这里插入图片描述

内存管理计算机资源

  1. 管理 CPU,由于 CPU 是执行程序的,而内核把运行时的程序抽象成进程,所以又称为进程管理。
  2. 管理内存,由于程序和数据都要占用内存,内存是非常宝贵的资源,所以内核要非常小心地分配、释放内存。
  3. 管理硬盘,而硬盘主要存放用户数据,而内核把用户数据抽象成文件,即管理文件,文件需要合理地组织,方便用户查找和读写,所以形成了文件系统。
  4. 管理显卡,负责显示信息,而现在操作系统都是支持 GUI(图形用户接口)的,管理显卡自然而然地就成了内核中的图形系统。
  5. 管理网卡,网卡主要完成网络通信,网络通信需要各种通信协议,最后在内核中就形成了网络协议栈,又称网络组件。
  6. 管理各种 I/O 设备,我们经常把键盘、鼠标、打印机、显示器等统称为 I/O(输入输出)设备,在内核中抽象成 I/O 管理器。

内核要想管理和控制这些硬件就要编写对应的代码,通常这样的代码我们称之为驱动程序

宏内核结构

宏内核就是把以上诸如管理进程的代码、管理内存的代码、管理各种 I/O 设备的代码、文件系统的代码、图形系统代码以及其它功能模块的代码,把这些所有的代码经过编译,最后链接在一起,形成一个大的可执行程序

这个大程序里有实现支持这些功能的所有代码,向用户应用软件提供一些接口,这些接口就是常说的系统 API 函数。而这个大程序会在处理器的特权模式下运行,这个模式通常被称为宏内核模式。
在这里插入图片描述
宏内核提供内存分配功能的服务过程,具体如下:

  1. 应用程序调用内存分配的 API(应用程序接口)函数。
  2. 处理器切换到特权模式,开始运行内核代码。
  3. 内核里的内存管理代码按照特定的算法,分配一块内存。
  4. 把分配的内存块的首地址,返回给内存分配的 API 函数。
  5. 内存分配的 API 函数返回,处理器开始运行用户模式下的应用程序,应用程序就得到了一块内存的首地址,并且可以使用这块内存了。

宏内核缺点:

  • 没有模块化,没有扩展性、没有移植性,高度耦合在一起,一旦其中一个组件有漏洞,内核中所有的组件可能都会出问题。
  • 开发一个新的功能也得重新编译、链接、安装内核。

宏内核优点 :

  • 性能很好,因为在内核中,这些组件可以互相调用,性能极高。

微内核结构

微内核功能尽可能少:仅仅只有进程调度、处理中断、内存空间映射、进程间通信等功能。

实际的进程管理、内存管理、设备管理、文件管理等服务功能,做成一个个服务进程。和用户应用进程一样,只是它们很特殊,宏内核提供的功能,在微内核架构里由这些服务进程专门负责完成。

微内核定义了一种良好的进程间通信的机制——消息
应用程序要请求相关服务,就向微内核发送一条与此服务对应的消息,微内核再把这条消息转发给相关的服务进程,接着服务进程会完成相关的服务。服务进程的编程模型就是循环处理来自其它进程的消息,完成相关的服务功能。
在这里插入图片描述
微内核提供内存分配功能的服务过程:

  1. 应用程序发送内存分配的消息,这个发送消息的函数是微内核提供的,相当于系统 API,微内核的 API(应用程序接口)相当少,极端情况下仅需要两个,一个接收消息的 API 和一个发送消息的 API。
  2. 处理器切换到特权模式,开始运行内核代码。
  3. 微内核代码让当前进程停止运行,并根据消息包中的数据,确定消息发送给谁,分配内存的消息当然是发送给内存管理服务进程。
  4. 内存管理服务进程收到消息,分配一块内存。
  5. 内存管理服务进程,也会通过消息的形式返回分配内存块的地址给内核,然后继续等待下一条消息。
  6. 微内核把包含内存块地址的消息返回给发送内存分配消息的应用程序。
  7. 处理器开始运行用户模式下的应用程序,应用程序就得到了一块内存的首地址,并且可以使用这块内存了。

微内核优点:

  • 系统结构相当清晰利于协作开发。
  • 系统有良好的移植性
  • 微内核有相当好的伸缩性、扩展性,因为那些系统功能只是一个进程

微内核缺点:

  • 同样是分配内存,一来一去的消息带来了非常大的开销,当然各个服务进程的切换开销也不小。这样系统性能就大打折扣。

分离硬件的相关性

  • 分离硬件的相关性,就是要把操作硬件和处理硬件功能差异的代码抽离出来,形成一个独立的软件抽象层,对外提供相应的接口,方便上层开发。
  • 硬件平台相关的代码都抽离出来,放在一个独立硬件相关层中实现并且定义好相关的调用接口,再在这个层之上开发内核的其它功能代码,就会方便得多,结构也会清晰很多。
  • 操作系统的移植性也会大大增强,移植到不同的硬件平台时,就构造开发一个与之对应的硬件相关层。这就是分离硬件相关性的好处。

3.CPU工作模式

  1. 实模式,单道程序能掌控计算机所有的资源,仅支持 16 位地址空间,分段的内存模型,对指令不加限制地运行,对内存没有保护隔离作用。
  2. 保护模式,保护模式包含特权级,对指令及其访问的资源进行控制,对内存段与段之间的访问进行严格检查,没有权限的绝不放行,对中断的响应也要进行严格的权限检查,扩展了 CPU 寄存器位宽,使之能够寻址 32 位的内存地址空间和处理 32 位的数据,从而 CPU 的性能大大提高。
  3. 长模式,又名 AMD64 模式,在保护模式的基础上,把寄存器扩展到 64 位同时增加了一些寄存器,使 CPU 具有了能处理 64 位数据和寻址 64 位的内存地址空间的能力。长模式弱化段模式管理,只保留了权限级别的检查,忽略了段基址和段长度,而地址的检查则交给了 MMU。

4. 虚拟地址与真实地址

虚拟地址到物理地址的转换

虚拟地址到物理地址的转换: 软硬件结合的方式实现,它就是 MMU(内存管理单元)。MMU 可以接受软件给出的地址对应关系数据,进行地址转换。
在这里插入图片描述
上图中展示了 MMU 通过地址关系转换表,将 0x80000~0x84000 的虚拟地址空间转换成 0x10000~0x14000 的物理地址空间,而地址关系转换表本身则是放物理内存中的

分页模型: 把虚拟地址空间和物理地址空间都分成同等大小的块,也称为页,按照虚拟页和物理页进行转换。根据软件配置不同,这个页的大小可以设置为 4KB、2MB、4MB、1GB
在这里插入图片描述
结合图片可以看出,一个虚拟页可以对应到一个物理页,由于页大小一经配置就是固定的,所以在地址关系转换表中,只要存放虚拟页地址对应的物理页地址就行了。

MMU

MMU 即内存管理单元,是用硬件电路逻辑实现的一个地址转换器件,它负责接受虚拟地址和地址关系转换表,以及输出物理地址。
在这里插入图片描述
上图中,程序代码中的虚拟地址,经过 CPU 的分段机制产生了线性地址,平坦模式和长模式下线性地址和虚拟地址是相等的。

MMU 页表

地址关系转换表——页表。它描述了虚拟地址到物理地址的转换关系,也可以说是虚拟页到物理页的映射关系,所以称为页表。

页表中并不存放虚拟地址和物理地址的对应关系,只存放物理页面的地址,MMU 以虚拟地址为索引去查表返回物理页面地址,而且页表是分级的,总体分为三个部分:一个顶级页目录,多个中级页目录,最后才是页表,逻辑结构图如下.
在这里插入图片描述
从上面可以看出,一个虚拟地址被分成从左至右四个位段:

  • 第一个位段索引顶级页目录中一个项,该项指向一个中级页目录,
  • 然后用第二个位段去索引中级页目录中的一个项,该项指向一个页目录,
  • 再用第三个位段去索引页目录中的项,该项指向一个物理页地址,
  • 最后用第四个位段作该物理页内的偏移去访问物理内存。这就是 MMU 的工作流程。

5.Cache与内存

内存: 是计算机中用于存储数据和程序的硬件设备。它允许计算机快速读取和写入数据,以及执行指令。内存通常被描述为随机访问存储器(RAM),因为它可以随时存取任意地址的数据。

Cache(缓存):是一种高速缓存存储器,位于计算机内部的CPU和主内存之间。它用于暂时存储处理器频繁访问的数据和指令,以减少对主内存的访问次数,从而提高系统性能。

Cache的工作流程如下:

  1. CPU首先从L1 cache开始查询所需数据或指令。

  2. 如果在当前级别的cache中命中,则直接读取数据或指令;否则,查询下一个级别的cache。

  3. 依次查询L2和L3 cache,如果命中,则从相应的cache中读取数据或指令,并将其复制到更高一级的cache中供以后使用。

  4. 如果都未命中,则从主内存中读取数据或指令,并将其复制到所有级别的cache中。

  5. 写操作时,若数据存在于缓存中,则直接更新缓存中的数据,否则写入主内存。

  6. 当缓存满时,采用替换策略腾出空间,常见的策略包括LRU、FIFO和随机替换等。

Cache的结构

在这里插入图片描述
这是一颗最简单的双核心 CPU,它有三级 Cache,第一级 Cache 是指令和数据分开的,第二级 Cache 是独立于 CPU 核心的,第三级 Cache 是所有 CPU 核心共享的。

Cache缓存一致性问题

  1. 一个 CPU 核心中的指令 Cache 和数据 Cache 的一致性问题。
    • 对于程序代码运行而言,指令都是经过指令 Cache,而指令中涉及到的数据则会经过数据 Cache。
    • 修改了内存地址 A 这个位置的代码,这个时候通过储存的方式去写的地址 A,所以新的指令会进入数据 Cache。但是接下来去执行地址 A 处的指令的时候,指令 Cache 里面可能命中的是修改之前的指令。
    • 所以,这个时候软件需要把数据 Cache 中的数据写入到内存中,然后让指令 Cache 无效,重新加载内存中的数据。
  2. 多个 CPU 核心各自的 2 级 Cache 的一致性问题。
    • 为了解决这些问题,硬件工程师们开发了多种协议,典型的多核心 Cache 数据同步协议有 MESI 和 MOESI

6. 解决数据同步的四种方法

  1. 原子变量,在只有单个变量全局数据的情况下,这种变量非常实用,如全局计数器、状态标志变量等。我们利用了 CPU 的原子指令实现了一组操作原子变量的函数。
  2. 中断的控制。当要操作的数据很多的情况下,用原子变量就不适合了。但是我们发现在单核心的 CPU,同一时刻只有一个代码执行流,除了响应中断导致代码执行流切换,不会有其它条件会干扰全局数据的操作,所以我们只要在操作全局数据时关闭或者开启中断就行了,为此我们开发了控制中断的函数。
  3. 自旋锁。由于多核心的 CPU 出现,控制中断已经失效了,因为系统中同时有多个代码执行流,为了解决这个问题,我们开发了自旋锁,自旋锁要么一下子获取锁,要么循环等待最终获取锁。
  4. 信号量。如果长时间等待后才能获取数据,在这样的情况下,前面中断控制和自旋锁都不能很好地解决,于是我们开发了信号量。信号量由一套数据结构和函数组成,它能使获取数据的代码执行流进入睡眠,然后在相关条件满足时被唤醒,这样就能让 CPU 能有时间处理其它任务。所以信号量同时解决了三个问题:等待、互斥、唤醒。

请添加图片描述

7. linux启动流程

请添加图片描述

8. 如何划分与组织内存?

内存分段和分页是常用的内存管理方式,每种方式都有其优缺点。

内存分段的优点包括:

  1. 允许多个进程共享物理内存;
  2. 可以按需加载数据,减少内存占用量;
  3. 避免了内部碎片问题;
  4. 段表可以动态增长,更加灵活。

其缺点包括:

  1. 内存分段需要消耗额外的时间和内存来维护段表,增加了系统开销;
  2. 分段容易造成外部碎片,导致内存利用率下降。

内存分页的优点包括:

  1. 可以实现虚拟内存扩展,允许程序使用比实际物理内存更多的内存;
  2. 分页保证了固定大小的块,使操作系统更高效地管理内存,减少内存碎片;
  3. 分页也可以实现内存保护,防止程序意外修改数据。

其缺点包括:

  1. 分页需要消耗额外的时间和内存来维护页表,增加了系统开销;
  2. 分页可能会导致内部碎片问题,即一页的大小过大时,可能会浪费一部分内存空间;
  3. 分页不支持多个进程共享同一个页面。

分段与分页的区别,发现段长度不一,容易产生内存碎片、不容易和硬盘换入换出数据,更不能实现扁平化的虚拟内存地址空间,其实现在所有的商用操作系统都使用了分页模式管理内存。

9. Linux:伙伴系统如何分配内存?(管理物理内存页)

伙伴系统(buddy system) :是一种常见的内存管理算法,它负责管理Linux中物理内存的分配和释放。

  • 这个算法通过将可用的内存按照2的幂次方进行划分,形成一棵二叉树,每个节点表示某个内存块的大小和状态(已分配或者未分配)。
  • 当需要分配一块内存时,伙伴系统会找到大小最接近并且大于等于所需内存大小的空闲内存块,并将其划分为两个伙伴块(即大小相同的两个子节点),其中一个被分配给请求方,另一个则重新加入伙伴系统的空闲链表中。
  • 当一个内存块被释放时,伙伴系统会检查该块的伙伴是否也为空闲状态,如果是,则将两个伙伴块合并成一个更大的块,继续加入空闲链表中。这种方式可以避免内存碎片问题,并提高内存利用率。

在 Linux 物理内存页面管理中,连续且相同大小的 pages 就可以表示成伙伴

请添加图片描述

上图中,首先最小的 page(0,1)是伙伴,page(2,3)是伙伴,page(4,5)是伙伴,page(6,7)是伙伴,然后 A 与 B 是伙伴,C 与 D 是伙伴,最后 E 与 F 是伙伴。有了图解,你是不是瞬间明白伙伴系统的伙伴了呢?

Linux 下怎样分配物理内存页面首先要找到内存节点,接着找到内存区,然后合适的空闲链表,最后在其中找到页的 page 结构,完成物理内存页面的分配。

  • 内存节点(pglist_data):Linux 对 NUMA (Non-Uniform Memory Access,非一致性内存访问)进行了抽象,它可以将一整块连续物理内存的划分成几个内存节点,也可以把不是连续的物理内存当成真正的 NUMA。
  • 内存区(zone):因为硬件的限制,Linux 内核不能对所有的物理内存页统一对待,所以就把属性相同物理内存页面,归结到了一个区中。zone中 free_area 结构的数组,这个数组就是用于实现伙伴系统。
  • free_area :实现伙伴系统 , 其中 MAX_ORDER 的值默认为 11,分别表示挂载地址,连续的 page 结构数目为 (0 - 2^11) 1,2,4,8,16,32……最大为 1024。free_area 结构中又是一个 list_head 链表数组, 该数组将具有相同迁移类型的 page 结构尽可能地分组,有的页面可以迁移,有的不可以迁移,同一类型的所有相同 order 的 page 结构,就构成了一组 page 结构块。
    请添加图片描述

linux 分配内存页面的主要函数__alloc_pages_nodemask:

  1. 准备分配页面的参数;
  2. 进入快速分配路径;
  3. 若快速分配路径没有分配到页面,就进入慢速分配路径。
  • 快速路径分配:是指当有足够的可用内存时,Linux 伙伴系统将首选最接近所需大小的可用块进行分配。该过程非常快速,因为它只涉及到查找可用块的信息。

  • 慢速路径分配:是指当没有足够的可用内存时,Linux 伙伴系统必须通过一系列操作来拆分较大的块,直到找到适合所需大小的块。此过程可能需要遍历整个内存二叉树,因此速度较慢。虽然慢速路径分配比快速路径分配要慢,但它确保了内存的高效利用,因为它能够将较大的块拆分成更小的块以满足内存请求。

10. Linux:SLAB如何分配内存?(kmalloc() 函数使用)

伙伴系统的缺点

  • Buddy提供了以page为单位的内存分配接口,这对内核来说颗粒度还太大了,所以需要一种新的机制,将page拆分为更小的单位来管理
  • 所以,引入slab分配器是为了弥补内存管理粒度太大的不足

slab 能解决什么问题?

  • slab分配需要解决的是内存的内部碎片问题。

slab 分配例子:

  • 比如我需要一个100字节的连续物理内存,那么内核slab分配器会给我提供一个相应大小的连续物理内存单元,为128字节大小(不会是整好100字节,而是这个档的一个对齐值,如100字节对应128字节,30字节对应32字节,60字节对应64字节)

slab分配器的基本思想

  • 先利用页面分配器分配出单个或者一组连续的物理页面,然后在此基础上将整块页面分割成多个相等的小内存单元,以满足小内存空间分配的需要。当然,为了有效的管理这些小的内存单元并保证极高的内存使用速度和效率。

什么是object?

  • object是slab内存分配器对外提供的申请内存的基本单位。slab内存分配器从buddy system申请了buddy之后,会将其拆分成一个个object,并缓存在kmem cache实例的cpu_cache中,用户申请内存时,其实获取的就是一个个object。
  • 一旦object缓存耗尽,就会重新从buddy system申请slab,并将其拆分成object,放入内存池。

什么是cache?

  • slab内存分配器中的cache跟硬件cache无关,是一个纯软件的概念。
  • slab内存分配器有两种cache,一个是slab的cache,一个是object的cache。
  • slab内存分配器从buddy system获取页面后,会将其加入kmem cache的node节点,这个就是slab的cache;
  • 将slab拆分成多个object,并将object加入kmem cache的cpu_cache内存池,这个就是object的cache;
  • 可以看到这两种cache实际是对共同的物理页面的两种缓存形式。

slab内存结构
请添加图片描述

  • SLAB 对象: 在 SLAB 分配器中,它把一个内存页面或者一组连续的内存页面,划分成大小相同的块,其中这一个小的内存块就是 SLAB 对象
  • array_cache结构 : 是每个CPU一个array_cache类型的变量,cpu_cache是用于管理空闲对象的
  • kmem_cache 结构:SLAB 管理头用 kmem_cache 结构来表示
  • kmem_cache_node 结构 : 每个内存节点对应一个,它就是用来管理 kmem_cache 结构的。kmem_cache_node 中的三个链表,它们分别挂载的 pages,有一部分是空闲对象的 page、还有对象全部都已经分配的 page,以及全部都为空闲对象的 page。这是为了提高分配时查找 kmem_cache 的性能。

SLAB 分配对象的过程

  1. 根据请求分配对象的大小,查找对应的 kmem_cache 结构,接着从这个结构中获取 arry_cache 结构,然后分配对象。
  2. 如果没有空闲对象了,就需要在 kmem_cache 对应的 kmem_cache_node 结构中查找有空闲对象的 kmem_cache。
  3. 如果还是没找到,最后就要分配内存页面新增 kmem_cache 结构了。

请添加图片描述

11. Linux进程与进程调度

请添加图片描述
为什么要用红黑树来组织调度实体? 这是因为要维护虚拟时间的顺序,又要从中频繁的删除和插入调度实体,这种情况下红黑树这种结构是最好的

12. 虚拟文件系统管理文件

VFS(Virtual Filesystem Switch)虚拟文件系统 :是一个内核软件层,在具体的文件系统之上抽象的一层,表现为能够给各种文件系统提供一个通用的接口,使上层的应用程序能够使用通用的接口访问不同文件系统,同时也为不同文件系统的通信提供了媒介。

  • VFS 提供了一个抽象层,让不同的文件系统表现出一致的行为。对于用户空间和内核空间的其他部分,这些文件系统看起来都是一样的:文件都有目录,都支持建立、打开,读写、关闭和删除操作,不用关注不同文件系统的细节。
  • 操作具体文件时,VFS 会根据需要调用具体文件系统的函数。从此文件系统的细节就被 VFS 屏蔽了,应用程序只需要调用标准的接口就行了。请添加图片描述

VFS 的数据结构:

  • 超级块结构:表示文件系统 , 中包含了 VFS 规定的标准信息,也有具体文件系统的特有信息
  • 目录结构:表示文件路径的 ,目录也是文件,需要用 inode 索引结构来管理目录文件数据。目录文件数据:名称、类型(文件或者目录)、inode 号
  • 索引结点结构:inode 结构表示一个文件索引结点,它里面包含文件权限、文件所属用户、文件访问和修改时间、文件数据块号等一个文件的全部信息,一个 inode 结构就对应一个文件
  • 文件对象结构:进程打开的文件实例结构,存放已打开的文件和进程之间交互的信息,包含了我们非常熟悉的信息,如访问模式、当前读写偏移等
    请添加图片描述

猜你喜欢

转载自blog.csdn.net/qq_44814825/article/details/130378650
今日推荐