Contents

OS learning

隔离与同步

研究操作系统绕不开的就是隔离二字。为了多任务之间的隔离,有了进程的概念。为了用户程序与系统核心程序之间的隔离,有了用户态和内核态的区分。

不同的任务之间有时又需要协同合作,所以严格隔离的同时,又需要实现有条件的资源同步。进程之间需要数据同步,所以有了进程间通信。用户态和内核态之间需要相互通信,所以有了陷入、中断、异常等机制。

进程

进程

用户态 & 内核态

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

操作系统的权限隔离依赖于硬件的权限隔离。如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或者机器断电,一方面正在写入的数据会破碎无用,另一方面如果当前存储单元已被标记为已用,那么这块存储单元将被废弃。机器重启后重新进行写操作分配了新的存储单元,而旧的单元由于被标记为已用也不会再被用。相当于我们的存储设备空间凭空少了一部分,如果多来几次谁都是受不了的。

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

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

多线程调度

多线程与锁