深入解析Mac OS X & iOS 操作系统 学习笔记(九)

内核架构

内核的基础知识

所有现代的操作系统在设计师都包含一个称为内核(kernel)的组件,是整个系统的核心。内核就是操作系统。从高层次看,所有运行的应用程序实际上是内核的“客户”,而内核向客户提供各种服务,即系统调用(system call)。
内核也是一个调度器。所有现在的操作系统都是抢占式的多任务(preemptive multitasking)系统,而实际上,程序的数目可以远多于处理器(或核心)的数目。内核必须判断哪一个程序(或进程、或线程)运行在哪一个处理器/核心上。
内核提供的另一组服务就是安全服务(security service),用户最容易感受到的安全服务是许可(permission)和权限(right),这些机制可以保证系统中各种资源的完整性、隐私性以及公平实用。

内核架构

巨内核

巨内核(monolithic,也称为宏内核、单内核)架构是“经典”的内核架构,而且仍然是UNIX和Linux 世界采用的主要内核架构。巨内核采取的方式是将所有的内核功能:不论是基础功能还是高级功能,全部放在一个地址空间中。在这个架构的内核中,线程调度和内存管理,以及文件系统、安全管理、甚至设备驱动都在一起。Linux的内核实现非常接近于标准的UN*X 内核,其架构图如下:

深入解析Mac OS X & iOS 操作系统 学习笔记(九)_第1张图片
Linux 内核架构.png

所有的内核功能都实现在同一个地址空间中。为了进一步优化,巨内核不仅将所有的功能都组织在同一个地址中,还将这个地址映射到每一个进程的内存中。在巨内核架构中,从用户态到内核态的切换非常高效,基本上就是一次线程切换的开销,内核的内存页面映射在所有进程的地址空间中,也就是说,除非硬件强制将内核态和用户态隔离外,两者之间其实没有任何分别。所有的进程,不论所有者或者功能,都包含一份内核内存的拷贝,就好像包含共享库的拷贝一样。此外,这些拷贝都映射到了同一组物理页面,而且是常驻内存的物理页面。这样不仅结束了宝贵的RAM,而且避免了系统调用时产生重大开销,这一点尤其重要,使得系统调用好用户态融为一体。

微内核

尽管微内核(microkernel)不是那么常见,但是微内核却是我们要关注的重点。因为XNU的核心组件Mach就是一个微内核系统。

一个微内核只包含最核心的功能,代码也是最精简的。内核值复制完成最最关键的部分:任务调度、内存管理,其他功能都交给外部服务程序(通常是用户态)完成。服务程序之间完全被隔离开,服务程序之间的所有通讯都是由消息传递(message passing)完成。消息传递机制允许将消息(通常是透明的)以及后续消息投递至服务程序的队列中排队,服务程序可以从队列中取出消息并且依次处理。微内核架构如下图:

深入解析Mac OS X & iOS 操作系统 学习笔记(九)_第2张图片
微内核架构.png

微内核的优点:

  • 正确性
  • 稳定性和健壮性
  • 灵活性

微内核的缺点:

  • 性能差
混合内核

混合内核(hybrid kernel)试图结合两种内核(巨内核和微内核)的好处。内核的最核心部分支持底层服务,包括调度、进程间通信(inter-process cimmunication,IPC)和虚拟内存,是自包含的,这一部分像微内核一样。所有其他的服务都实现在这个核心者外,但是也在内核态中,而且和这个核心在同一个内存空间中。

OS X 的 XNU 是一个混合内核,XNU 的核心 Mach 最早是一个真正的微内核,现在Mach 的原语仍然是围绕着消息传递的基础构建的。然而,消息通常是以指针形式传递的,因此没有昂贵的复制操作。这是因为大部分服务现在都在同一个地址空间中执行(因此也被归为巨内核)。类似的,建立在 Mach 之上的BSD层一直都是巨内核,而且这个子系统也在同一个地址空间中。

用户态和内核态

Intel 架构:ring

基于Intel的系统提供了所需要的基于硬件的分离。从286处理器开始,Intel 引入了“保护模式”的概念(在386中得到了极大的增强)。保护模式强制使用了4个“ring”。这些“ring0”指的就是权限级别,分别从0到3编号。这些“ring”以同心圆的方式组织,最内层的ring为 ring 0,最外层的为ring 3。 ring 0的权限最高,通常被称为超级用户模式(Supervisor mode)。只有最受信任的代码才能运行在处理器的ring 0上。随着ring级别递增,安全限制越多,权限也越低。
ring 0对应的是内核态,ring 3对应的是用户态。 ring 1 和 ring 2 预留给操作系统服务使用,但是在实际中却没有使用。ring 编号较小的代码可以随意切换到编号更大的ring,但是决不允许编号更大的ring切换到编号更小的ring,除非编号较小的ring已经建立好了一个调用门(call gate)机制。

ARM架构:CPSR

ARM 处理器使用了一个特殊的寄存器:当前程序状态寄存器(current program status register,CPSR)来定义处理器所在的模式,ARM 处理器有以下主要的操作模式,如下表:

模式|表示模式的位|用途
---|---|
USR|10000|用户模式 --- 不允许操作
SVC|10011|管理器模式(默认的内核模式)
SYS|11111|系统模式 --- 同用户模式,但是允许写入CPSR
FIQ|10001|快速中断请求
IRQ|10010|普通中断请求
ABT|10111|终止模式 --- 错误的内存访问
UND|11011|未定义模式 --- 非法/不支持的指令

USR 是唯一没有特权的模式,在内核通常运行组SVC模式。在任何特权模式中,都可以直接访问CPSR寄存器,隐藏只要修改CPSR中的模式位即可切换模式。在用户态,必须使用一种用户态/内核态转换机制。

内核态/用户态转换机制

用户态和内核态的转换机制有两种类型:

  • 自愿转换:当应用程序要求内核服务的时候,应用程序可以进行一个调用进入内核态,通过一个预定义的硬件指令可以开始进入内核态的切换。这些内核服务称为系统调用
  • 非自愿转换:当发生执行异常、中断或处理器陷阱的时候,代码的执行会被挂起,并且保留发生错误时候的完整状态。控制权被转交给预定义的内核态错误处理程序或中断服务程序(interrupt service rountine,ISR)
Intel 上的陷阱处理程序
  • ** 异常:陷阱/错误/终止**
    在Intel 架构上,中断向量的前20个单元定义为异常(exception);异常指的是处理器在执行代码的时候可能碰到的所有非正常状况。异常的情况分为以下3种

  • 错误(fault):指令遇到一个可以纠正的异常,并且处理器可以重新启动这条出现异常的指令,这种异常称为错误。一个常见的例子是页错误,当某个虚拟内存地址表示的页面不足物理RAM中是发生页错误。出现错误的时候,执行错误处理程序,完成之后返回到生成这个错误的指令。

  • 陷阱(trap):类似于错误,但是错误处理完成后返回发送陷阱指令之后的那条指令。

  • 中止(abort):不可重启指令。

  • 中断
    中断是由 CPU 中的一个特殊组件产生的。这个组件称为可编程中断控制器(Programmable Interrupt Controller,PIC),在更加高级的CPU中,称之为高级可编程中断控制器(Advanced PIC,APIC)。PIC 接收来自系统总线上的设备的消息,然后将消息分拣到某一条中断请求(Interrupt Request,IRQ)线上去。当产生中断的时候,PIC将相应的中断标记为活跃。在这个中断被一个函数(称为中断处理程序或中断服务程序)处理或服务完成之前,这条中断线一直保持活跃状态。处理这个中断的函数要负责重置这条线的状态。
    以前的PIC(称为XT-PIC)只要16条中断线,范围为0~15。而现代的APIC 允许多大255条这样的中断线。如果需要的话,IRQ线可以被多个设备共享。
    一般性的经验法则,只要满足一下条件,中断就会被分发出去:

  • 对应的中断请求线当前不忙(中断线忙说明前一个中断还没处理完)或被屏蔽(被屏蔽说明处理器或处理器核心忽略这条中断线)

  • 没有编号更低的中断线状态为忙

  • 本地CPU/处理器核心没有(通过底层CLI/STI汇编指令)禁用所有的中断

  • 在Intel 架构上XNU对陷阱和中断的处理
    XNU实际上将Intel 的异常统一称为“陷阱”。大部分操作系统内核都不会为每一个陷阱设置独立的处理程序,而是为所有的陷阱设置一个处理程序,然后这个处理程序通过switch( ) 进行不同的处理,或者根据预定义的表跳转到不同的函数。XNU的做法也是如此,定义了TRAP宏和USER_TRAP宏,这些宏通过其他有一些宏(IDT_ENTRY_WRAPPER和PUSH_FUNCTION)设置栈。

  • ARM上陷阱处理程序
    ARM的架构比Intel 简单多了。在ARM的角度看,任何非用户态都是通过一个异常或者一个中断进入的。因此系统调用是利用SVC指令通过模拟的中断完成的。SVC 是“SuperVisor Call”的简称,过去成SWI,即SoftWare Interrupt(软件中断),其实过去的这个名称更为准确:当时这个指令执行的时候,CPU 自动将控制权转交给及其的陷阱向量,在陷阱向量中有一个预定义的内核指令正在等待,通常是分支跳转到某个具体处理程序的指令。
    内核要负责设置好CPU 支持的所有模式的陷阱处理程序。iOS 的内核要依照下表列出的陷阱处理程序设置ExceptionVectorsBase,从而完成陷阱处理程序的设置。

偏移量 异常 处理程序
0x00 重置 _fleh_reset
0x04 未定义指令 _fleh_undef
0x08 软件中断 _fleh_swi
0x0c 预取中止 _fleh_prefabt
0x10 数据终止 _fleh_dataabt
0x14 地址异常 _fleh_addrexc
0x18 中断请求 _fleh_irq
0x1c 快速中断请求 _fleh_fiq
自愿的内核转换

当用户态的程序需要内核服务的时候,会发出一个系统调用,系统调用将控制权转换交给内核。实现系统调用的方法有两种:

  • 模拟中断:Intel 架构遗留的传统方法。 ARM 采用这种方法(通过SVC/SWI指令)

  • 指令:SYSENTRY 和 SYSCALL

  • ARM上的自愿内核转换
    ARM架构没有专门的系统调用指令,仍然是使用系统调用。内核加载的时候会覆盖所有的陷阱处理,其中包括软件中断(SWI)处理程序。当用户态程序执行SVC指令的时候,控制权转交给处理程序fleh_swi,CPU 进入内核模式。

系统调用的处理

大部分人都熟悉POSIX系统调用。然而在XNU中,POSIX 系统调用只是4种系统调用的类别之一:

SYSCALL_CLASS 处理程序 包含的调用
UNIX unix_syscall[64] POSIX/BSD 系统调用:“经典”的系统调用,是XNU 的BSD API接口
MACH mach_call_munger[64] Mach 陷阱:直接调用XNU 的 Mach 核心的接口
MDEP machdep_syscall[64] 机器的相关的调用:用于访问处理器特定的功能
DIAG diagCall[64] 诊断调用:用于底层的内核诊断。通过引导diag启用
  • POSIX/BSD 系统调用
    POSIX/BSD 的接口是 XNU 暴露出来的主要接口,这些在内部称为“UNIX 系统调用”或“BSD 调用”

  • unix_syscall
    BSD 系统调用的流程如下:
    (1)验证传入的状态快照和处理器架构是否相匹配
    (2)通过current_task 获得当前BSD 进程的数据结构。检查这个BSD 进程确实存在
    (3)如果系统调用号为0,那么说明这是一个非直接的系统调用。相应地修正参数
    (4)系统调用传递的参数应该是64位的。对于64未的处理程序来说,如果系统调用的阐述不能全部通过寄存器进行传递(即参数数目大于6的情况),则需要一些额外的工作,多余的参数需要复制到栈上。在32位的处理程序中,需要对参数进行mung操作。mung指的是从用户态复制参数的同时保持32位/64位兼容性的过程
    (5)执行sysent表中的系统调用
    (6)在很罕见的情况下,系统调用可能会表示需要重新执行,重新执行是由pal_syscall_restart( )处理的
    (7)系统调用返回的错误码放在返回寄存器中(Intel:EAX/RAX 寄存器;ARM:R0 寄存器)
    (8)系统调用处理通过thread_exception_return( )返回(在iOS 上通过load_and_go_user 返回),这个函数的处理和return_from_trap( )相同,在返回的过程中会处理 AST

  • sysent
    sysent 表维护了 BSD 的系统调用。这个表是一个由名称类似的数据结构组成的数组

  • ** Mach 陷阱**
    在32位OS X 或 iOS 上系统调用编号为负,或者在64位系统上类别为Mach,那么内核流程进入Mach陷阱处理的流程,而不是BSD 系统调用的处理。Mach 陷阱的处理程序成为mach_call_munger[64]

  • mach_call_munger
    Mach 陷阱是有mach_mall_munger[64] 处理的。UNIX 和 Mach 调用的参数都要进程 mung 处理,32 位的 unix_syscall 依然包含 munging 操作的代码

  • mach_trap_table
    mach_trap_table 是一个mach_trap_t 结构体数组。之后跟着是mach_syscall_name_table,其中保存了对应的名字。

  • 机器相关的调用
    除了Mach 陷阱和 UNIX 系统调用外, XNU 还包含机器相关的调用。
    这些调用的主要功能是和CPU 缓存相关(例如MMU 失效相关的指令和数据缓存相关的指令),确实反映了“机器相关”的本质

  • machdep_call_table
    机器相关的调用也有自己的分发表:machdep_call_table,大部分机器相关的调用在Intel架构上没有用,在32位架构上,有设置LDT和GTD的调用。在64 位架构上,只要一个调用:tthread_fast_set_cthread_self函数。这个函数设置处理器的控制寄存器c13和c0。在ARM中调用机器相关调用的方法是:将R12 设置为0x8000 0000,然后在R3 中传入调用编号

  • 诊断调用
    这一类系统调用专门用于诊断。这一类系统调用只定义了一个诊断调用:diagCall/diagCall64。在Intel 时代,XNU 的diagCall 功能只支持一个诊断代码:dgRuptStat(#25),用于查询或重置每一个CPU的中断统计信息

你可能感兴趣的:(深入解析Mac OS X & iOS 操作系统 学习笔记(九))