和其他系统一样,Mac OS X的系统由一个firmware启动。PowerPC中使用的是Open Firmware,而x86则换成了EFI。这两个firmware有着和老式的BIOS类似的功能,但比起BIOS只能用于引导系统相比,Open Firmware和EFI可以自定义启动过程以及诊断启动问题。Firmware在PowerPC上加载一个名叫BootX的bootloader引导Mac OS X启动,在x86上EFI会加载boot.efi。Bootloader会在各自的firmware环境中运行,并最终启动内核。
在bootloader运行后,计算机的控制权就会交给Mac OS X内核。有一些人会把Mac OS X内核称作Darwin,然而Darwin和Mac OS X是不同的两个系统。如前文所说Darwin是一个开源版的Rhapsody系统,也就是Mac OS X的前身。Darwin本身是一个开源软件的集合,有着来自苹果自己的或者NeXT的开源组件,也有GNU或X11这样久负盛名的开源软件。而Mac OS X以Darwin为基础,加入了大量苹果私有的软件,例如Cocoa和Quicktime。而这些又是苹果其他图形化软件的基础。因此,Darwin不能运行Mac OS X的应用,包括Safari和Xcode。然而,由于Darwin是一个完整的Unix内核,使用X11运行Unix软件还是可以的。
真正的Mac OS X内核是XNU,也就是之前所述的Mach为核心,使用BSD环境,并有着面向对象的驱动环境的系统。然而XNU并不会包含驱动代码,驱动会被包装在一个Darwin package中运行。因此严格来讲Mac OS X的内核比XNU的代码还要多。但总的来说,驱动也是XNU的一部分,因此下文会用XNU指代整个内核。内核可以被分为以下几个部分
- Mach – 底层服务层
- BSD – 系统程序接口层
- I/O Kit – 驱动运行时
- libkern – 内核中的函数库
- libsa – 通常在系统启动时使用到的内核函数库
- The Platform Expert – 硬件抽象模块
- Kernel extensions – 大量使用I/O Kit的驱动和少数不使用I/O Kit的扩展
其中,内核的核心就是Mach。Mach提供了最重要的底层服务,包括
- 一些硬件抽象
- 处理器管理,包括多处理器多任务调度
- 抢占式多任务处理以及对任务(Mach的进程)与线程的支持
- 虚拟内存管理,包括分页,保护,共享内存和内存继承
- IPC机制
- 软实时任务处理,用于支持多媒体程序等对计时敏感的程序
- 内核debug支持
- 控制台I/O
尽管网络上很多人会一口认定Mach是一个微内核,但从第一章的历史可以看出Mach3才是一个真正的微内核。苹果使用的正是Mach3,但其实苹果也并没有按照微内核的方式使用Mach3。真正的微内核通常要求只有内核本身在内核地址空间中,其他程序(包括BSD内核和驱动)都在用户空间。而在Mac OS X中,BSD内核和I/O Kit其实都和Mach在同一个内存空间。然而,这些组件都在逻辑上分离以保证不会冲突。
Mach之上是一个BSD系统,但如上文所述BSD内核和Mach内核在某种程度上缠绕在一起,因此BSD在某种程度上依然同时存在在用户空间和内核空间。BSD的职责包括
- 提供BSD的进程模型
- 信号
- 用户系统和全县系统
- POSIX API
- 异步I/O(AIO)
- BSD的syscall
- TCP/IP网络栈,BSD socket和防火墙
- 网络内核扩展,将BSD的网络架构融入XNU
- 大量的文件系统支持,包括virtual file system支持
- System V和POSIX的IPC机制
- 内核密码学加密框架
- FreeBSD的系统通知机制,即kqueue/kevent
- 使用fsevents的文件系统变化通知机制,被spotlight使用
- ACL和kauth授权框架
- 一些同步原语
BSD的一些代码主要是为了保证Unix和Mac OS X的兼容性,因此有一些BSD的机制会被Mach取代。例如尽管BSD的进程模型保留了下来,但BSD并不会调度这些进程。取而代之的是每一个BSD进程都会映射到Mach任务上运行。一些BSD syscall例如fork则会调用Mach的syscall创建新的任务。系统自动确保BSD和Mach的进程/任务数据结构是统一的。因此BSD的进程在Mac OS X里就像胶水一样连起了Mach和Unix。
尽管大多数情况下BSD的syscall会自动转换成Mach的syscall,有时Mac OS X也会直接使用Mach的syscall,也被称作Mach trap,去完成映射内存或复制硬盘等操作。
内核的下一个部分是I/O Kit,也就是驱动框架。I/O Kit是一个使用C++的子集写成的驱动框架。I/O Kit使用的C++没有异常,没有多重继承,没有多个构造器,没有模板,没有初始化列表也没有RTTI(虽然I/O Kit自带一个简单的RTTI)。简单的说就是只保留了最关键的面向对象的部分。
整个I/O Kit由两个内核中的C++库libkern和IOKit以及一个用户空间的框架IOKit.framework组成。整个驱动运行时是模块化的,因此每个I/O Kit的驱动不仅用于驱动硬件,也用来表示多个硬件间的关系。例如硬盘分区是通过多个I/O Kit类之间的关系表示的,包括硬盘,硬盘控制器,总线等等其他一系列驱动。I/O Kit提供以下功能
- 一系列程序接口,包括对应用程序和用户空间驱动开放的I/O kit接口
- 连接设备的功能,包括通过ATA/ATAPI,火线,图形接口,HID,网络,PCI和USB连接
- 面向对象的抽象
- 热插拔和动态设备管理支持
- 电源管理
- 驱动的抢占式调用,多线程,多任务处理和内存保护
- 动态寻找和加载驱动
- 一个记录已经初始化的对象信息的数据库(I/O Registry)
- 一个记录所有支持的I/O Kit类的数据库(I/O Catalog)
标准设备例如鼠标和键盘通常不需要额外的I/O Kit驱动支持,或者只需要一个用户空间驱动,因为I/O Kit已经提供了USB等连接的支持。
Libkern和libsa是内核中的两个函数库。libkern主要提供了I/O kit需要的C++类和一些其他方便内核软件开发的支持类。其中最重要的是OSObject类,这是整个Mac OS X内核其他类的基类。OSObject实现了动态类型和加载内核模块的功能。除此以外libkern还提供了
- 动态分配,构建和解构支持的对象,包括数组,布尔型和字典
- 原子操作和辅助函数,比如bcmp(),memcmp()和strlen()
- 字节操作函数
- 追踪所有类的每一个实例状态
- 缓解脆弱基类问题(fragile base-class problem)
脆弱基类问题是C++的常见问题,由于C++的面向对象依赖已知的内存布局(基类的变量位置,虚表等),如果只修改基类而不重新编译用到这个基类的子类可能会破坏子类的行为。libkern允许开发者预留一些空间用于在未来加入新的变量和函数而不会破坏子类。
libsa是一个辅助内核启动阶段和内核模块加载的库。sa指的是stand-alone,也就是加载一个独立的程序-系统内核。Mac OS X的内核拓展通常使用/usr/libexec/kextd加载(在Mac OS 11中已经被System extension取代,kextd和旧的内核模块已经被废弃,但整个Mac OS X系列一直到10.15都还支持)。在系统启动早期kextd还不可用,这时libsa就担当了kextd的一部分功能。libsa的功能有
- 简单的内存分配
- 二分检索
- 排序
- 各种字符串处理函数
- 符号remangle
- 通过依赖图自动寻找内核拓展依赖
- 解压内核拓展并校验
要注意的是libsa只在启动阶段存在,一旦kextd启动libsa就会被删除。就算在启动阶段libsa的函数也没有一个接口可以被其他程序调用。
最后一个重要组件是Platform Expert,这是一个主板相关的驱动,用于让系统知道现在运行的平台。I/O kit会给Platform Expert注册一个nub。一个nub定义了物理或逻辑设备的接入点或通讯频道。这个设备可以是总线和显卡,或者仲裁器和电源管理驱动。注册完毕后IOPlatformExpertDevice实例会成为设备树的根。这个根nub会加载平台有关的驱动,为每一个总线注册一个nub,然后I/O Kit会给每一个总线nub加载对应的驱动,并通过驱动发现连接的设备,就这样注册每一个设备。