购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.3 如何形成一个内核

Linux内核并不是唯一的内核,也并不是唯一的一种内核。实际上,Linux内核属于Monolithic Kernel的一个实现,这种内核还包括UNIX系列(BSD、SunOS等)、DOS和Windows 9x系列,还有OpenVMS、XTS-400、z/TPF等一些不常见的系统内核。

从学术上看内核的种类包括Exokernel、Nanokernel、Microkernel、Hybridkernel、Monolithickernel和Anykernel这6种。Windows NT(9x之后的所有)都属于Hybridkernel,因为它系统功能的一部分在内核中,一部分在用户空间中。

内核提供的核心功能只有一个:资源管理。资源管理大致分为3种,即CPU资管调度、内存管理、I/O设备管理(内存管理也归在此类)。

1.3.1 内核形成过程

我们分析一下在一个裸板上从头写一个操作系统要经过的过程。

首先,我们肯定要把汇编的启动代码封装一下,然后写一些启动的C代码,内存需要汇编来初始化,才能向其中加载内容。但这也并不是绝对的,我设计过的一个作品就是一个裸片的专用操作系统的实现,但是并没有使用任何的汇编,而是将要操作的寄存器地址直接定义为变量,你能在很多嵌入式系统中看到类似的定义。

加载之后,我们可以在内存中执行代码了。但是此时内存中是无章法的一大片连续块,有的平台甚至还不连续,物理地址具体会被映射到什么样的线性地址,还需要查看各个芯片的手册才知道。很多芯片还有特殊的映射结构和启动逻辑,例如三星的SucurityZone,我们在启动的早期必须要具体问题具体对待,这也是各种各样驱动存在的原因。要使用C语言就必须为内存划分块(全局区、代码区等),我们可以让GCC去完成这个工作,但是通常需要手动布局一下。当涉及堆,很大的内存时,就需要考虑内存分配算法和页的划分了。有了页的划分就得有缺页中断的实现了。这部分就实现了内存管理。一般来说,用GCC作为主要工具来写操作系统的时候,只需要实现堆的内存分配算法。而如果是裸片程序,堆都可以不用实现,直接使用一大块连续的内存,自己在内存中组织自己的数据结构就可以。事实上,在很多嵌入式开发的时候都是强制要求在运行过程中不得使用堆内存的,而是在程序执行的开始时分配大块内存自己用。

然后我们需要初始化外部的设备,由于外部的设备都是寄存器控制的,寄存器通常是功能复用的,通过写入功能号来执行功能,通过查看一些寄存器的位来确定状态。如PCI这种总线硬件更复杂,还会有单独映射的配置空间,网卡有相对通用的MII中间层。针对一类硬件的访问就需要封装,而分类类型可大可小,例如我说写入到存储设备是一类操作,但是存储设备也分为SSD和磁盘,磁盘又可以细分为很多不同的类型。这样逐层封装就形成了硬件抽象层。如果同样是鼠标,不同的商家的寄存器排列不同,那么我们就得针对每一种鼠标实现一个驱动,然后驱动的上层接口与硬件抽象层相符。于是驱动和硬件抽象层的概念就产生了。

如果到此为止,只实现了基本的硬件管理和硬件抽象层,“Nanokernel”就产生了。而一个操作系统可以多进程,进程间还要通信,各个进程之间还要调度。内核里实现了进程概念这一步,就是“Microkernel”。再实现一些文件系统、网络协议栈等就是“Monolithickernel”了,如果在用户端也实现了一些系统级功能,就是“Hybridkernel”了。

1.3.2 Exokernels和Anykernel

Exokernels是最微小的内核,其功能仅限于限制对资源访问进行复用和保护。对硬件做最基本的抽象,允许应用无限权限的访问硬件。也就是说,Exokernel只是为应用提供一种最小化的硬件抽象,怎么使用是使用者说了算,也就根本不知道进程这种概念了。

NetBSD实现了第一个Anykernel(Rump Kernel)。Anykernel本身既是内核又是程序。作为程序它向基于它的其他程序提供内核的功能。作为内核,因为它本身就包含内核的功能。也就是说,Anykernel在任何操作系统上是可移植的。例如在Linux系统上可以使用Rump Kernel的TCP/IP协议栈,下层直接用DPDK,上层的应用调用Rump Kernel就可以完成socket协议栈的操作。这种方式本质是一种虚拟化,与其他的虚拟化方式不同的是,其他的虚拟化虚拟的是硬件,希望运行不同的软件,而Anykernel的思路是虚拟软件,可以运行在不同的操作系统上。Rump Kernel的实现包含了文件系统、协议栈和设备管理。

1.3.3 内核为何使用C语言

关于C语言与C++,大部分认为C语言执行效率高。但很多人做过实验,如果C++不使用RTTI,C++的效率也不会比C语言的效率低太多(25%左右)。还有人说C++虽然拥有强大的STL,但是对于极度追求效率的情况,STL不能用。大部分人的心态是,学C++出身的就经常埋怨Linux的C语言代码乱的一塌糊涂,崇拜自己的各种敏捷,面向对象原则,代码不如C++精简,连STL或者Boost都用不上,还有软件工程相关问题都是被他们抱怨的“重灾区”,但是这部分人大多数都没有长时间地写过C语言。

面向对象的实现方法不止C++一种语言,C语言也可以做到,只是不如C++那样浑然天成。重要的是C语言做的面向对象,由于封装性差,一个大型的结构体里面什么都有,看起来比较混乱。虽然可以用多层次的结构体定义在一定程度上让代码更加可读,但是内核中大部分情况下并没有这么做。而用C++封装的结构体,可以设计得很潇洒,vector<People>会比list*people能传递更多的信息。利用C++的封装特性带来的好处在视觉上显而易见,基本不需要考虑内存布局,但在C语言的结构体定义中到处都有对内存上的考虑,例如通过结构体成员找到结构体本身的container_of;在结构体最后添加一个0长度的数组;在结构体的开头添加一个list*节点。乍一看C++也能做到,毕竟C语言的所有关键字C++都是完全支持的。但是问题是这并不是面向对象的思路,有一种编程思路叫作ODP(面向数据编程),就是典型的C语言更加合适的用途。说C语言比C++高效,并不一定是说C++本身效率低,而是其所代表的面向对象的思路执行效率低。举个例子,代码如下。

我们对这个aList的a变量进行遍历,比起下面这个写法,代码如下。

一定是下面这个写法的效率高,原因在于缓存。所有的计算机系统都有缓存行,第二种写法被调入缓存的全部是a。而第一种写法还调入了b,遍历起来第一种写法比第二种写法,是将缓存减小了一半。这就是ODP比OOP高效的地方。而当你不选用OOP时,C++比C语言多的最大优势就荡然无存。

C++的确有强大的STL和Boost库。但是这种库都是通用的,大型的企业用到后期对执行效率开始敏感的时候,通用的东西几乎不可能在所有的情况下都是最优的选择。包括内存算法、进程调度、网络应用,简单地说就是要针对不同应用调整参数,例如TCP在服务于Samba时要关闭Nagle才能提高效率,但有的应用Nagle则是提高效率的利器。而C++的STL给与我们对库运行策略调整的能力过小。内核是底层的系统,使用这种程度上通用的东西确实是不现实的。最通用的数据结构应该是list了,在数据组织上也是非常轻量的,甚至在服务于不同的应用时还有hlist和llist等变体。这种变体对效率的追求不是C++的再封装所能满足的。

C++最骄傲的特性是:封装、不过于关心内存、虚函数、继承。虚函数确实是在机制上就存在效率问题,一用它就得多一个虚函数表,如果这不算是小事,函数调用跳转带来的缓存的刷新可真不是件小事了。我们只要用了虚函数,很难控制哪个地方就刷一下缓存,这对于内核来说是不可接受的,因为内核要实现一个算法的时候都要小心翼翼地关心每个函数调用的操作是不是性能上潜在的一个陷阱。至于继承,一个写了很久C++代码的人都有一个体会:除非是做大型应用软件,否则业务的建模真没那么多变体。对于内核开发来说,虚函数基本就是可有可无的鸡肋,可能会有几个比较重要的大型功能框架可以用到这个特性,典型的是驱动体系,但这完全不构成因此而选择这门更复杂语言的理由。如果我们只是开发大型商用平台式应用软件,恐怕都要考虑内存布局,甚至是有的效率敏感代码还要刻意挑出来用C语言或者汇编重写。

我们在使用C++的时候有很多设计模式,有很多编程技巧。它们带来的效果大部分是架构清晰、代码量变少、易于修改。但是C++带来的更多的是内存的使用量飙升和代码体积的增大。C语言的代码确实多一些,但是过程式的代码比起C++要到处去找到底是哪个虚函数或者哪个子类在起作用要方便得多。毕竟C++有的是运行时才确定的,而我们看代码的时候是静态的。这有一个本质的原因:C语言可以看静态代码得出编程的全部意图,但C++不能或者很难。而内核的开发者真正要动手写的非常少,几乎都是稍微改改代码或者是编写一下驱动。所以对于内核来说,即使是深度使用内核工作的人,大部分也是使用者,而并非开发者,那么追求敏捷的意义又有多大呢?

笔者也曾诟病内核的很多C语言代码写的比较混乱。比如电梯算法的框架代码,得非常认真地长期阅读才能大概体会作者的意图。然而我知道,即使我很容易看懂了电梯算法的代码,我也不会修改它。它提供了很多参数,然而大部分情况下我通过调整参数就可以满足自身需求。即使在全球范围内,真正对于重写这块代码或者对写自己的电梯算法有需求的公司恐怕都屈指可数,修改和维护工作自有作者自己在做。如果一个员工告诉上司他修改了很多电梯算法的框架时,公司的领导估计是不敢用的。做到这个程度的产品基本是嵌入式产品,嵌入式产品的软件一发布就不容易更新软件了。如果可以通过修改参数来完成,怎么会有决策者采用冒险的修改内核代码的方式呢?毕竟技术在产品面前,是要服务于产品的。

笔者也是C++的拥护者,甚至是狂热者,日常的软件、大部分的公司产品开发都会选择C++或者Java,而绝对不会选择C语言。然而在深入学习内核功能之后,如果可以重新选择内核使用的语言,笔者也会选择C语言。虽然笔者也曾经用C++写过裸片操作系统,也长期使用过eCos。面向对象确实是编程的一大进步,那我们也得思考一下数据库大行其道的为什么不是实体联系模式,而是关系模式呢?我们可以采用炫酷的技术,因为技术确实推动了生产力革命,当然也不能迷信技术,工程和技术是完全不同的两回事。 tcpIguKVDb97CzXCqs0KF+nZrRwa866Ta4xeC1CFPdxNDf0LHbROi8GxFvlbOf0h

点击中间区域
呼出菜单
上一章
目录
下一章
×