第 1 章 操作系统概述

第 1 章 操作系统概述

本章首先从宏观上介绍操作系统由哪几部分组成,然后介绍编写操作系统必须掌握的知识,最后再简要介绍本书操作系统。

1.1 什么是操作系统

操作系统这个概念非常宽泛,不论是办公、生活使用的设备与计算机,还是机械工业生产制造使用的仪器仪表,它们都装有操作系统。哪怕是只有一条指令的单片机,也可以称作嵌入式操作系统。从这个宏观意义上出发,操作系统和硬件设备就可以区分开,它们要么是只有硬件电路的裸机,要么是含有操作系统的硬件电路。(这里的硬件电路指含有处理器的可编程电路。)

对于只有一条指令的单片机来说,它被称为操作系统,未免显得太过牵强。在大多数人眼里,操作系统应该由功能强大高效运转的核心、万能的驱动程序、绚丽的操作界面、舒适简洁的操作方式及方便实用的工具组成。可在当年,操作系统却是一个连硬盘都没有而只有一些简单逻辑门电路的怪物。从操作系统的发展史来看,它经历了单任务系统、批处理系统、分时操作系统、实时操作系统、嵌入式操作系统以及时下最流行的云系统等阶段。随着时代的发展,硬件在不断更新换代,操作系统也在不断演化,操作系统的功能会因为应用场景不同而具有不同的特点,但它的根本目的依然是为了方便人们对硬件设备的使用与交互。

1.2 操作系统的组成结构

一款功能完备、方便易用的操作系统,是由一套庞大的结构组成的,图1-1描述了操作系统的整体结构。

图1-1 操作系统整体结构图

从图1-1可以看出,操作系统由内核层与应用层两部分组成。内核层主要由引导启动、内存管理、异常/中断处理、进程管理、设备驱动、文件系统等模块组成,而系统API库和应用程序则属于应用层的范畴。之所以将内核层和应用层分开,是因为内核层主要负责控制硬件设备、分配系统资源、为应用层提供健全的接口支持、保证应用程序正常稳定运行等全局性工作。而应用层主要负责的是人机交互工作。下面将对各个模型逐一进行介绍。

  • 引导启动。引导启动是指,计算机从BIOS上电自检后到跳转至内核程序执行前这一期间执行的一段或几段程序。这些程序主要用于检测计算机硬件,并配置内核运行时所需的参数,然后再把检测信息和参数提交给内核解析,内核会在解析数据的同时对自身进行配置。如图1-1所示,使用横线将引导启动模块与其他内核层模块分隔开,是考虑到引导启动模块只是为了辅助内核启动,而并非真正属于内核。一旦内核开始执行后,引导程序便再无他用。如果把内核比作卫星的话,那么引导程序就相当于运载火箭,卫星进入轨道后,火箭就完成了它的使命。引导启动程序曾经分为两部分——Boot和Loader,现在通常把两者的功能合二为一,并统称为BootLoader。

    目前,比较流行的引导启动程序有Grub和Uboot等,它们的功能都比较强大,用户可以通过它们自带的终端命令行与之进行简单的交互,此举为控制内核的加载和使用提供了诸多便利。

  • 内存管理。内存管理单元是内核的基础功能,它的主要作用是有效管理物理内存,这样可以简化其他模块开辟内存空间(连续的或非连续的)的过程,为页表映射和地址变换提供配套函数。

    Linux内存管理单元的伙伴算法,算是一种稳定成熟的内存管理算法,它可以长时间保持内存的稳定分配,防止内存碎片过多。还有内存线性地址空间的红黑树管理算法,它将原有的线性地址结构转换为树状结构以缩短搜索时间,同时又在每次插入新节点时调整树的高度(或者深度),来维持树的形状进而保证搜索时间的相对稳定,该算法既兼顾搜索时间损耗又兼顾插入时间损耗。因此,Linux选择红黑树这种近似平衡树来代替之前的AVL树(绝对平衡树)也是出于这方面的考虑。

  • 异常/中断处理。此处的异常是指处理器在执行程序时产生的错误或者问题,比如除零、段溢出、页错误、无效指令、调试错误等。有的异常经过处理后,程序仍可继续执行,有的则不能继续执行,必须根据错误类型和程序逻辑进行相应的处理。而中断处理是指处理器接收到硬件设备发来的中断请求信号并作出相应处理操作。这部分内容与外围硬件设备关系非常密切,它的处理效率会影响操作系统整体的执行速度。通常,中断处理会被分为中断上半部和中断下半部。中断上半部要求快速响应中断,在取得必要的数据和信息后尽早开启中断,以使处理器能够再次接收中断请求信号。中断下半部被用来执行剩余中断内容,像数据解析、驱动程序状态调整等更耗时的内容均在这里完成。为了让更紧迫的进程优先执行,中断下半部还可将处理内容安放在一个进程中,以让更高优先级的进程得到快速执行。

  • 进程管理。说到进程,想必会有人疑惑进程和程序的区别。程序是静静地躺在文件系统里的二进制代码,属于静止状态。一旦把这个程序加载到操作系统内运行,它就变成了进程。进程是程序的运行状态,所以它会比程序拥有更多管理层面的信息和数据。

    说到进程管理功能,不得不提进程调度策略,一个好的进程调度策略,会提高程序的执行效率和反应速度。现代Linux内核的发展从早期的O(1)调度策略,到楼梯调度策略,再到现在的CFS完全公平调度策略,随着调度策略逐步升级,进程的执行效率也越来越高。进程管理的另一个重要部分是进程间通信。进程间通信有很多种方法,如SIGNAL信号、管道、共享内存、信号量等,这些通信机制各有特点,互相弥补不足。

  • 设备驱动。随着硬件设备的不断增多,与之对应的设备驱动程序也渐渐占据了操作系统的很大一部分空间。为了给开发和使用设备驱动程序带来方便,不管是Linux操作系统还是Windows操作系统,它们都为驱动程序提供了一套或几套成熟的驱动框架供程序员使用。同时,为了便于驱动程序的调试、提高即插即用设备的灵活性及缩减内核体积,操作系统逐渐把驱动程序从内核中移出,仅当使用驱动时再将其动态挂载到内核空间,从而做到驱动程序即插即用。这样一来大大缩小内核体积,加快系统启动速度。

    设备驱动程序会与内存管理、中断处理、文件系统及进程管理等多个模块共同协作。为了让硬件设备给应用程序提供接口,设备驱动程序几乎调用了内核层的所有资源。这也是开发操作系统的目的之一,即方便人们与设备交互。

  • 文件系统。文件系统用于把机械硬盘的部分或全部扇区组织成一个便于管理的结构化单元。此处的扇区也可以是内存块,这样便组成了一个RAMDisk(内存式硬盘)。这样一个内存式硬盘单单在文件读写速度上就比普通机械硬盘高出一个数量级,其显而易见的缺点是掉电后数据全部丢失。不过与它的优点相比,这个缺点是完全可以忍受的,比如Linux内核的sys文件系统便是在RAMDisk中创建的。

    文件系统的种类也是纷繁复杂的,像上面提到的sys文件系统,还有大家耳熟能详的FAT类文件系统,以及Linux的EXT类文件系统,它们对扇区的组织形式虽各具特色,却都是为了给原生操作系统提供方便、快捷的使用体验而设计的。

  • 系统调用API库。系统调用API库接口有很多规范标准,比如Linux兼容的POSIX规范标准。对于不同的接口标准来说,其定义和封装的函数实现是不一样的。不管怎么说,系统调用API库最终都是为了给应用程序提供简单、快捷、便于使用的接口。

  • 应用程序。应用程序包括我们自己安装的软件和系统提供的工具、软件与服务。

    在众多应用程序中,比较特殊的一个应用程序是系统的窗口管理器,它主要用于管理图形界面的窗口,具体包括窗口的位置布局、鼠标键盘的消息投递、活动窗口仲裁等功能。

    • 窗口的位置布局负责控制窗口的比例大小、显示位置、标题栏及按钮等一系列与窗口的显示效果相关的功能。
    • 键盘鼠标的消息投递负责将键盘鼠标发送来的消息发往到活动窗口,这个过程会涉及窗口管理器对活动窗口的仲裁。
    • 活动窗口仲裁会依据鼠标采用的仲裁模式(包括鼠标跟随式、鼠标按下式等)来确定正在操作的窗口。

1.3 编写操作系统需要的知识

鉴于操作系统是与硬件设备紧密相连的软件程序,所以操作系统的编写自然会涉及软件和硬件两个方面。

  1. 硬件方面

    首先,我们要根据硬件电路掌握处理器与外围设备的电路组成,通俗一点说,就是处理器和外围设备是怎么连接的。当掌握电路组成后,进而可以知道处理器如何控制外围设备,以及采用何种方式与它们通信。对于ARM这类片上系统而言,它们与外围设备的连接方法非常灵活,所以这部分内容必须要掌握;而通用的PC平台的连接方法相对固定得多。因此,编写一个PC平台的操作系统,对硬件电路的掌握要求会比较宽松。

    其次,既然清楚了硬件电路的连接,下一步就是阅读这些硬件设备的芯片手册。芯片手册会详细描述芯片的硬件特性、通信方式、芯片内部的寄存器功能,以及控制寄存器的方法。不管何种硬件平台,硬件设备的芯片手册都同等重要,不了解这部分内容也就无法与硬件设备通信。一般情况下,操作系统开发人员会更关注处理器如何与这些硬件设备通信、如何控制它们的寄存器状态,而硬件工程师则会更关注芯片的工作环境、温度、工作电压、额定功率等硬件特性指标。

    所以在硬件方面,掌握硬件电路、处理器和外围设备的芯片手册即可。其中,处理器芯片手册会介绍如何初始化处理器、如何切换处理器工作模式等一系列操作处理器的信息与方法,这些知识为操作系统运行提供技术指导。硬件芯片手册会对设备上的所有寄存器功能进行描述,我们根据这些寄存器功能方可编写出驱动程序。

  2. 软件方面

    至于软件方面,只要熟练运用汇编语言和C语言就足够编写操作系统了。

    汇编语言主要用于控制和配置处理器,例如引导启动处理器、配置处理器运行状态、进程切换、中断和异常处理程序、设备I/O端口操作等必须操作寄存器的工作,或者是对性能要求极为苛刻的场景,这些工作C语言几乎无法实现。

    C语言是编写操作系统的主要开发语言,它以简单、高效、使用灵活等特性深受底层开发人员的喜爱。而且,它在内嵌汇编语言以及与汇编语言的相互调用方面都表现得非常自然,只要遵循C语言的标准方法就能实现这些功能。

    除了熟练使用开发语言之外,操作系统作为所有资源的管理者,一些灵活高效的算法也是必不可少的。从基础的链表结构,到树状结构,再到图状结构,操作系统会根据不同的应用场景有选择地使用它们。此外,一些灵活的编程技巧也必不可少。像内核异常处理程序的错误对照表,其原理是在程序容易出错的地方提前写出错误处理函数,并将出错地址和处理地址记录在错误对照表内。当错误发生时处理器会自动捕获错误地址,操作系统会从错误对照表里检索出对应的处理地址,并加以执行。这个过程必须借助链接脚本巧妙设计地址空间,才能组建错误处理对照表。

    综上所述,编写一个操作系统必备的知识并不多,只需掌握汇编语言和C语言,能够看懂硬件电路图和硬件芯片手册即可。如果期望操作系统运行得又快又稳,那么还需要在兼顾空间开销和性能损耗的同时适当使用高效算法。所以,编写一个操作系统不难,难的是通过巧妙的方法让它运行得更高效、更人性化。

1.4 本书操作系统简介

在了解操作系统的结构组成和编写操作系统需要的知识后,接下来将对本书即将实现的操作系统加以介绍。本书以Linux操作系统作为主要参考对象,来编写一个操作系统雏形,并将其运行在物理平台上。此举既可以对学习代码量巨大的Linux内核有所帮助,又可以在本系统基础上通过动手实践对理论加以验证,进而做到举一反三。而且,使用物理平台运行操作系统,会大大增加读者的成就感和对操作系统的理解能力,同时还能排除虚拟平台带来的差异和问题。下面将本操作系统分为引导启动、内核层与应用层三部分,并逐一对它们进行介绍。

  • 引导启动。引导启动程序将使用NASM汇编语言编写,实现U盘引导启动、文件系统识别、系统内核加载、内存容量检测、显示模式的检测与设置、处理器运行模式切换、页表配置等功能,进而完成系统内核运行前的准备工作。此环节涉及的关键技术点有BIOS中断调用、VBE功能获得和设置、FAT12/32文件系统结构解析、E820内存地址分布、U盘与磁盘的区别、处理器体系结构探索等。
  • 内核层。内核层部分是操作系统的重头戏。正如前文所述,本系统将参考Linux内核来编写一个功能相对健全的内核雏形,其中会涉及编译技术和链接技术来将程序划分出不同的代码空间。而且,系统内核还将配有内存管理模块、中断/异常处理模块、进程管理模块、多核通信模块、文件系统模块、外部设备驱动等一系列功能模块,成为一个可以正常工作且功能相对完整的系统内核。同时,本系统还将遵循POSIX规范标准,为应用层提供通用的编程接口(系统调用API)。
  • 应用层。应用层部分将实现Shell命令解析器和一些基础命令。既然内核层已经实现了系统调用API,那么这些应用程序便可在此基础上予以实现。

综上所述,虽然本书操作系统参考了Linux内核源码,但并非直接裁剪Linux内核源码而成!因为,这样会帮助读者在学习本书的同时,便于向Linux内核过渡。就像当年Linux参考Unix一样,我们的操作系统必须先有个健全的系统雏形,才能承载我们远大的梦想。

目录