1.3 Linux内核
1.3.1 宏内核和微内核
操作系统属于软件的范畴,来负责管理系统的硬件资源,同时为应用程序开发和执行提供配套环境。操作系统必须具备如下两大功能。
为多用户和应用程序管理计算机上的硬件资源。
为应用程序提供执行环境。
除此之外,操作系统还需要具备如下一些特性。
并发性:操作系统必须具备执行多个线程的能力。从宏观上看,多线程会并发执行,如在单CPU系统中运行多线程的程序。线程是独立运行和独立调度的基本单位。
虚拟性:多进程的设计理念就是让每个进程都感觉有一个专门的处理器为它服务,这就是虚拟处理器技术。
操作系统内核的设计在历史上存在两大阵营,一个是宏内核,另一个是微内核。宏内核是指所有的内核代码都编译成一个二进制文件,所有的内核代码都运行在一个大内核地址空间里,内核代码可以直接访问和调用,效率高并且性能好,如图1.3所示。而微内核是指把操作系统分成多个独立的功能模块,每个功能模块之间的访问需要通过消息来完成,因此效率没有那么高。比如,当时Linus学习的Minix就是微内核的典范。现代的一些操作系统(比如Windows)就采用微内核的方式,内核保留操作系统最基本的功能,比如进程调度、内存管理通信等模块,其他的功能全部从内核移出,放到用户态中实现,并以C/S模型为应用程序提供服务,如图1.4所示。
图1.3 宏内核架构
图1.4 微内核架构
Linus 在设计之初并没有使用当时学术界流行的微内核架构,而是采用实现方式比较简单的宏内核架构,一方面是因为Linux在当时是业余作品,另一方面是因为Linus本人更喜欢宏内核的设计。宏内核架构的优点是设计简洁和性能比较好,而微内核架构的优势也很明显,比如稳定性和实时性等。微内核架构最大的问题就是高度模块化带来的交互的冗余和效率的损耗。把所有的理论设计放到现实的工程实践中都是一种折中的艺术。Linux在20多年的发展历程中,形成了自己的工程理论,并且不断融入了微内核的精华,如模块化设计、抢占式内核、动态加载内核模块等。
Linux 内核支持动态加载内核模块。为了借鉴微内核的一些优点,Linux 内核在很早时就提出了内核模块化的设计。Linux内核中很多核心的实现或者设备驱动的实现都可以编译成一个个单独的模块。模块是被编译成的一个目标文件,并且可以在运行时的内核中动态加载和卸载。和微内核实现的模块化不一样,它不是作为一个特殊模块来执行的,而是和静态编译的内核函数一样,运行在内核态中。模块的引入给Linux内核带来了不少的优点,其中最大的优点就是很多内核的功能和设备驱动都可以编译成动态加载和卸载的模块,并且驱动开发者在编写内核模块时必须遵守定义好的接口来访问内核核心,这也使得开发一个内核模块变得容易很多。另一个优点是,很多内核模块可以设计成和平台无关的,比如文件系统等。相比微内核的模块,还有一个优点就是继承了宏内核的性能优势。
1.3.2 Linux内核概貌
Linux内核从1991年至2018年已有近27年的发展过程,从原来不到1万行代码到现在已经超过2 000万行代码。对于如此庞大的项目,我们在学习的过程中首先需要了解其整体的概貌,再深入学习每个核心子模块。
Linux内核总体的概貌如图1.5所示,一个典型的Linux系统可以分成三部分。
硬件层:包括CPU、物理内存、主板、磁盘和相应的外设等。
内核空间:包括Linux内核的核心部件,比如arch抽象层、设备管理抽象层、内存管理、进程管理、总线设备、字符设备以及与应用程序交互的系统调用层。
用户空间:这里包括的内容很丰富,如C语言库、应用程序和虚拟机等。
图1.5 Linux内核概貌
我们重点关注内核空间层中一些主要的部件。
(1)系统调用层
Linux内核把系统分成两个空间:用户空间和内核空间。CPU既可以运行在用户空间,也可以运行在内核空间。一些体系结构的实现还有多种执行模式,如x86体系结构有ring0 ~ring3这4种不同的执行模式。但是Linux内核只使用了ring0和ring3两种模式来实现内核态和用户态。
Linux 内核为内核态和用户态之间的切换设置了软件抽象层,叫作系统调用(System Call)层,其实每个处理器体系结构设计中都提供了一些特殊的指令来实现内核态和用户态之间的切换。Linux内核充分利用了这种硬件提供的机制来实现系统调用层。
系统调用层最大的目的是让用户进程看不到真实的硬件信息,比如当用户需要读取一个文件的内容时,编写用户进程的程序员不需要知道这个文件具体存放在磁盘的哪个扇区里,只需要调用open()、read()或mmap()等函数即可。
一个用户进程大部分时间运行在用户态,当需要向内核请求服务时,它会调用系统提供的接口进入内核态,比如上述例子中的open()函数。当内核完成了open()函数的调用之后会返回用户态。
(2)处理器体系结构抽象层
Linux内核支持多种体系结构,比如现在最流行的x86和ARM,也包括MIPS、powerpc等。Linux最初的设计只支持x86体系结构,后来不断扩展,到现在已经支持几十种体系结构。为Linux内核添加一个新的体系结构不是一件很难的事情,如最新的Linux 4.15内核支持RISC-V体系结构。Linux内核为不同体系结构的实现做了很好的抽象和隔离,也提供了统一的接口来实现。比如,在内存管理方面,Linux内核把和体系结构相关部分的代码都存放在arch/xx/mm目录里,把和体系结构不相关的代码都存放在mm目录里,从而实现完好的分层。
(3)进程管理
进程是现代操作系统中非常重要的概念,包括上下文切换(Context Switch)以及进程调度(Scheduling)。每个进程运行时都感觉完全占有了全部的硬件资源,但是进程不会长时间占有硬件资源。操作系统利用进程调度器让多个进程并发执行。Linux内核并没有严格区分进程和线程,而常用task_struct数据结构来描述。Linux内核的调度器的发展经历了好几代,从很早的O(n)调度器到Linux 2.6内核中的O(1)调度器,再到现在的CFS公平算法调度器。目前比较热门的讨论是关于性能和功耗的优化,比如ARM阵营提出了大小核体系结构,至今在Linux内核实现中还没有体现,因此类似EAS(Energy Awareness Scheduling)这样的调度算法是一个研究热点。
进程管理还包括进程的创建和销毁、线程组管理、内核线程管理、队列等待等内容。
(4)内存管理
内存管理模块是 Linux 内核中最复杂的模块,它涉及物理内存的管理和虚拟内存的管理。在一些小型的嵌入式 RTOS 中,内存管理不涉及虚拟内存的管理,比较简单和简洁。但是作为一个通用的操作系统,Linux内核的虚拟内存管理非常重要。虚拟内存有很多优点,比如多个进程可以并发执行、进程请求的内存可以比物理内存大、多个进程可以共享函数库等,因此虚拟内存的管理也变得越来越复杂。在 Linux 内核中,关于虚拟内存的模块有反向映射、页面回收、KSM、Mmap 映射、缺页中断、共享内存、进程虚拟地址空间管理等。
物理内存的管理也比较复杂。页面分配器(Page Allocator)是核心部件,它需要考虑当系统内存紧张时,如何回收页面和继续分配物理内存。其他比较重要的模块有交换分区管理、页面回收和OOM Killer等。
(5)中断管理
中断管理包含处理器的异常(Exception)处理和中断(Interrupt)处理。异常通常是指如果处理器在执行指令时检测到一个反常条件,处理器就必须暂停下来处理这些特殊的情况,如常见的缺页异常(Page Fault)。而中断异常一般是指外设通过中断信号线路来请求处理器,处理器会暂停当前正在做的事情来处理外设的请求。Linux内核在中断管理方面有上半部和下半部之分。上半部是在关闭中断的情况下执行的,因此处理时间要求短、平、快;而下半部是在开启中断的情况下执行的,很多对执行时间要求不高的操作可以放到下半部来执行。Linux内核为下半部提供了多种机制,如软中断、Tasklet和工作队列等。
(6)设备管理
设备管理对于任何的一个操作系统来说都是重中之重。Linux内核之所以这么流行,就是因为它支持的外设是所有开源操作系统中最多的。当很多大公司有新的芯片诞生时,第一个要支持的操作系统是Linux,也就是尽可能地在Linux内核社区里推送。
Linux内核的设备管理是一个很广泛的概念,包含的内容很多,如ACPI、设备树、设备模型kobject、设备总线(如PCI总线)、字符设备驱动、块设备驱动、网络设备驱动等。
(7)文件系统
一个优秀的操作系统必须包含优秀的文件系统,但是文件系统有不同的应用场合,如基于闪存的文件系统F2FS、基于磁盘存储的文件系统ext4和XFS等。为了支持各种各样的文件系统,Linux 抽象出了一个称为虚拟文件系统(VFS)层的软件层,这样 Linux 内核就可以很方便地集成多种文件系统。
总之,Linux内核是一个庞大的工程,处处体现了抽象和分层的思想,其代码的质量是值得我们深入学习的。