Contents

OS learning

开篇

最近在学MIT 6.S081 Operating System,受益匪浅。开篇文章记录一下对一些所思、所想,或是总结。预计长期缓慢更新。

OS Organization

Why user/kernel mode?

现今操作系统普遍拥有内核态与用户态两层,这么做在根本上是为了权限隔离。

操作系统的权限隔离依赖于硬件的权限隔离。如Intel CPU 把指令权限划分为ring0-ring3四个等级,操作系统内核跑在ring0,拥有最高权限。用户态跑在ring3,权限最低。

权限隔离来源于操作系统对用户程序的零信任。要想设计一个尽可能安全的操作系统,就必须假设用户程序是恶意的。用户程序想要进行一些涉及特权指令的敏感操作(操作进程、更改页表、使用文件系统等等)都必须经过内核把关,即通过syscall把控制权交给内核。内核确认操作合法后,运行特权指令,最后把控制权还给用户程序。如果内核认为操作非法,多半会直接终止程序运行。

权限隔离促成了进程间隔离。一个进程无法访问其他进程的内存空间,得益于页表机制。但也正因为权限隔离,致使一个进程无法访问或更改页表,从而给页表加了把锁。每个进程都认为自己独占整个物理内存,使得每个进程都运行在一个隔离的环境中,没有互相间的影响。

使用用户态、内核态两层设计也提高了整个系统的稳定性。当一个用户程序运行中出现异常或崩溃时,内核不受影响,可以继续维持整个机器的正常运转。但这也警示我们,内核本身必须要足够健壮,一旦内核出现panic,那就真是回天乏术了。

宏内核?微内核?

传统的操作系统都是基于宏内核的。所谓宏内核,首先给人的印象就是大。从代码量上看,宏内核的代码量达到微内核的几十倍不成问题。

宏内核意味着在内核中实现了丰富的功能,包括但不限于:

  • 为了管理硬盘数据,内核需要实现一个文件系统。

  • 为了合理分配使用内存,内核普遍采用页表机制实现虚拟地址到物理地址的映射。

  • 为了处理硬件消息,统筹各种硬件运作,内核需要实现一整套中断机制。

  • 为了实现多核CPU的并行运算,内核需要建立一个线程切换、管理系统。

  • 为了保证多线程下数据的安全访问,内核还需要可靠的锁机制。

  • 为了封装底层,给应用开发提供系统API,内核需要规划、设计各种系统调用。

这也会是这篇文章后续讨论的一些主题。

随着操作系统的不断发展,人们发现内核中被塞进的东西越来越多,越来越难以维护。终于有人不堪忍受,提出了微内核的概念。

微内核致力于尽可能地减小内核的体量。微内核内部主要做两件事:

  • 实现一个基于服务(service)的进程、内存空间管理系统

  • 实现高效的进程间通信(IPC)

至于宏内核中文件系统、页表机制等等,被从内核中剥离了出来,放在用户空间变成了插件,作为一个进程运行。这在实际上让代码模块化更好,更易于维护。并且由于内核十分精简,出bug、漏洞的可能性大大降低,使整个系统更加健壮。

当用户应用需要使用文件系统、页表机制等时,就相当于要和另一个进程通信,即IPC,所以IPC的效率决定了整个系统工作的效率。整个过程中内核仅参与调度、管理,而不执行实际的功能代码。

在一些嵌入式场景中,由于文件系统或其他插件根本用不到,微内核的精简就显示出了其优势,被广受欢迎。

但是,在如今主机/服务器领域,还是宏内核、以及一些披着微内核名字的混合内核的天下。原因有三:

  • 性能问题。微内核由于在IPC过程中需要更频繁地进出内核,运行效率比宏内核更低。不过在著名的L4微内核中,通过设计的优化能将效率提升到非常接近原生linux内核的地步,未来微内核是否能更进一步也未可知。

  • 插件问题。想要真正开发出一个微内核操作系统,最难的点不在于内核,而在于插件。那些被从宏内核中拿出来的东西必须全部重新设计、开发,这并不是一件简单的事。

  • 应用生态问题。没有足够的成果表明微内核优于宏内核,并且由于上一点的原因,插件体系一直都不够完善,放弃主流的宏内核,为微内核开发应用是件费力不讨好的事。

文件系统

可以想到,最早的计算机是不需要文件系统的。但随着计算任务的增大、计算机应用愈渐复杂,需要设备存储一些数据。一方面要求数据能存储得有条理,能够高效读写。另一方面又要能够保证数据安全性,即一旦计算机(os)突然崩溃/断电,数据能正常留存,且对以后数据正常读写没有影响。这就是文件系统所要做的事了。

想要设计一个文件系统,首先要从硬件层面来看。无论是固态硬盘还是磁盘或者其他什么存储设备都会被其驱动抽象为一个线性的存储空间,所有的数据/文件都只是这块线性空间的一些普通字节,怎么利用这些空间组织一些有意义的数据,是文件系统设计的第一步。

现有大多数文件系统都采用了文件元信息(metadata)和文件数据分离存储的方式。首先在线性的存储单元上进行划分,对每个存储单元进行编号,将前几个单元人为划定为元信息存储单元,后面大部分单元划定为数据存储单元。在元信息单元会保存相应数据单元所在位置的编号,使得二者连接起来。文件夹的实现可能会有悖于常识,文件夹在硬盘上的存储和普通文件别无二致,只不过存储的数据由文件条目组成,而不是数据条目。

为了丰富文件系统的功能,方便用户管理,向上层提供API,文件系统可能还会存储更多东西。比如为了方便用户监控硬盘上闲置存量,文件系统会专门划分出一个或几个存储单元,每一个byte存储一个0或1的标志,用来反应对应存储单元是否被占用,这通常被称为bitmap。

文件系统的安全性是重中之重。假如文件系统在进行写操作是内核突发panic或者机器断电,一方面正在写入的数据会破碎无用,另一方面如果当前存储单元已被标记为已用,那么这块存储单元将被废弃。机器重启后重新进行写操作分配了新的存储单元,而旧的单元由于被标记为已用也不会再被用。相当于我们的存储设备空间凭空少了一部分,如果多来几次谁都是受不了的。

一个可行的设计就是在写入硬盘之前加一层缓冲机制。所有需要存储的数据都会首先被写入一个缓冲区,缓冲区有一个标志位,当写入缓冲区完成后,设置标志位,开始进行提交,真正去写入硬盘,写入完成后取消标志位。我们可以模拟一个断电时的场景进行验证:如果在写入缓冲区时断电,则对存储区域没有影响,重新进行整个写入操作即可。如果在提交过程中断电,重启后标志位未被取消,文件系统会重新执行提交,数据并未丢失。即使在最后取消标志位时断电,重启后无非只是重写入一遍已经存在的数据。

在整个写入操作中,要防止多核场景下的数据竞争,势必需要锁的使用,需要更细致、深入的设计

Page Table

操作系统工作离不开内存,而内存管理离不开页表,所以页表机制是整个操作系统最重要的部分之一。

使用页表的直接目的是为了隔离。最初运行在实模式下的MSDOS是没有页表的,所有进程都可以直接访问自身寄存器可以表示的所有物理内存,这显然是不安全的。为了内存安全,人们想出了页表这个办法。与实模式的区别在于,用户程序只能访问虚拟地址,由内核来做从虚拟地址到物理地址的映射。在每一个用户进程看来,自己独享了整个物理内存,而感知不到其他进程。

页表机制的核心是分页。为了便于管理,操作系统普遍采用分页机制。一页的大小通常是4kb,这是由CPU(硬件)设计者决定的。操作系统内核需要考虑内存页如何分配的问题,而真正进行地址转换的是CPU上的MMU(MemoryManagementUnit)。每一个进程都有自己的页表