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