内核提供的核心功能只有一个——资源管理。资源管理大致分为3种,即CPU资管调度、内存管理、I/O管理。内核中包含了必须要被包含的功能,所谓的“必须要被包含的功能”就是频繁需要特权指令运行的内容。可以将偶尔需要用到特权指令运行的功能放到用户态,将拥有特权指令的功能放到内核态。
所谓操作系统内核,就是多任务彼此安全可靠、高性能运行的一种组织方式。VxWorks(实时操作系统)就是以全内核模式运行所有任务的,因为这个系统的定位就是对所有的任务都高度信任。内核态和用户态的本质区别是,内核态不希望让不可控的用户态的逻辑影响到整个系统的安全性和稳定性。宏内核、微内核和VxWorks在技术层面其实都是对任务的信任问题的解答。
无论是Mac OS的I/O Kit,还是Android的HAL都试图在开源中撕开一个缺口,既希望得到开源的优势,又希望得到企业的支持。因为所有人都不得不面对一个现实:很多硬件企业非常重视自己的设计,认为驱动的暴露会导致设计的泄露,从而影响企业在市场上的优势。
我们分析一下在一个裸板上从头写一个裸片程序的过程。
首先,我们要把CPU芯片和启动程序所必需的其他芯片进行寄存器设置层面的初始化,再写一些启动程序的C语言代码,因为内存需要初始化,才能向其中加载内容。可以将要操作的寄存器地址直接定义为C语言的宏,对寄存器的所有设置都封装为对C语言函数的调用,在很多嵌入式系统中都能看到类似的定义。
对内存的初始化会比较麻烦,目前,CPU的线性地址和物理地址是有区别的。线性地址是指CPU的寻址空间,例如32位的CPU的线性地址空间的大小是4GB。物理地址是指外设的真实访问地址,需要映射到线性地址才能被CPU直接访问。物理内存的空间大小并不一定要4GB,只要线性内存的空间大小是4GB就可以了。内存和其他外设的物理地址在程序启动时具体会被映射到什么样的线性地址,还需要查看各个芯片的应用手册才能知道。很多芯片还有特殊的映射结构和启动逻辑,在启动的早期必须要具体问题具体对待,这也是各种各样驱动程序存在的原因。
然后,我们就可以将裸片程序逻辑加载到内存,让CPU跳转到程序入口点,开始执行自己编写的逻辑程序。
要使用C语言就必须为内存划分块(全局区、代码区等),我们可以让编译器完成这个工作,但是通常需要手动布局。当涉及堆或者很大的内存时,就需要考虑内存分配算法和页的划分了。有了页的划分就会发现,自己实际申请的内存并不一定会使用,会有缺页异常。如果直接使用一大块连续的内存,自己在内存中组织自己的数据结构,那么C语言的堆的概念也可以不实现。很多时候嵌入式开发会强制要求在运行过程中不得使用堆内存,而在程序执行的一开始就分配大块内存供自己用。编译可以采用专门用于裸片编程的编译器,例如Keil开发工具。
最后,我们需要初始化外部的设备。外部的设备都是由寄存器控制的,寄存器通常是功能复用的,它通过写入功能号来执行功能,并通过查看一些寄存器的位来确定状态。如PCI这种总线硬件更复杂,还会有单独映射的配置空间,网卡有相对通用的MII中间层等,针对一类硬件的访问需要封装,而分类类型可大可小,逐层封装就形成了硬件抽象层。比如同样是鼠标,不同商家的寄存器排列不同,那么我们就得针对每一种鼠标都实现一个驱动,让驱动的上层接口与硬件抽象层相符。于是驱动和硬件抽象层的概念就产生了。为了响应不同外设的事件,需要提供程序中断的实现,同时CPU也会对程序中断和异常处理提出要求,这时中断子系统的概念就浮出水面了。
操作系统的设计源于需求,如果仅仅为了满足专用需求,那么即使是多任务,最后也很大概率是VxWorks的纯内核态方式。只有当通用需求产生的时候,区分用户态和内核态才有意义。