最近有对之前写的一些 APM 相关的代码进行复盘,在监控一些基础性能信息时,我们需要跟 Mach API 打交道,这篇文章会梳理一下 Mach 相关的概念。整篇文章内容都摘自 《Mac OS X 技术内幕》 ,该书也对 BSD,I/O Kit 以及文件系统都有比较详尽的介绍,虽然是描述 Mac 系统但大部分原理与 iOS 相同。

Mac OS / iOS 内核中的模块主要有:

  • Mach:服务层
  • BSD:系统编程接口提供者
  • I/O Kit:驱动程序的运行时环境
  • libkern:内核库

而 Mach 是 xnu 的核心,提供了至关重要的低级服务,它负责的方面包括:

  • 某种程度的硬件抽象
  • 处理器管理,包括对任务和线程的支持
  • 抢占式多任务,包括对任务和线程的支持
  • 虚拟内存管理,包括低级分页,内存保护,共享和继承
  • 低级 IPC 机制,它们是内核中所有消息传递的基础
  • 实时支持,允许时间敏感的应用程序在一定延迟界限内访问处理器资源
  • 内核调试支持
  • 控制台 I/O

Mach 内核简介

Mach 通过抽象系统硬件给更高层提供了一个虚拟机 VM 接口。它被设计成简单和可扩展的:它提供了一种 IPC 机制,该机制由内核提供的许多服务的构件,特别的是,Mach 的 IPC 特性与其虚拟内存子系统结合成了一起,导致了很多优化和简化。

从开发者角度看,Mach 具有五个基本抽象:

  • 任务 Task
  • 线程 Thread
  • 端口 Port
  • 消息 Message
  • 内存对象 Memory

除了提供基本内核抽象之外,Mach 还把多种其他的硬件和软件资源表示为端口对象,允许通过其 IPC 机制操作此类资源。

  • 整体计算机系统 Host
  • 单个物理 CPU 表示为处理器 Processor
  • 处理器集合 Processor Set

任务和线程

内核中,BSD 进程是一种数据结构,与 Mach 任务之间具有一对一映射关系。Mach 任务具有以下关键特性:

  • 是一种执行环境和静态实体。它不执行计算而是提供一个框架,其他实体(线程)可以在其中执行
  • 资源分配的基本单元,比如处理器访问权限,分页式虚拟地址空间,IPC 空间,异常处理程序,凭证,文件描述符,保护状态,信号管理状态和统计信息
  • 保护程序的边界。一个任务不能访问另一个任务的资源,除非前者使用某个良好定义的接口获得了明确的访问权限

在 Mach 中,线程是实际的执行实体,是任务中的控制流程的点。具有以下特性·:

  • 它在任务上下文内执行,代表任务内独立的程序计数器 -- 指令流。线程也是基本的可调度实体,具有关联的调度优先级和基本属性。每个线程都是抢占式并且独立于其他线程调度的
  • 线程执行的代码驻留在其任务的地址空间里
  • 每个任务都可能包含零个或多个线程,但每个线程都属于一个任务
  • 一个任务内的线程都共享任务的所有资源
  • 线程可能有它自己的异常处理程序
  • 每个线程都具有它自己的计算状态,包括处理器寄存器,程序计数器和栈
  • 线程使用内核栈处理系统调用,内核栈的大小是 16 kb

端口

Mach 端口是一个多方面的抽象。它是一个内核保护的单向 IPC 通道,容量和名称。传统上,在 Mach 中将端口实现为具有有限长度的消息队列。

任务需要持有合适的权限以操作端口,比如发送权限和接收权限。

消息

Mach IPC 消息是线程之间相互交换以进行通信的数据对象。Mach 中典型的任务间通信就是使用消息发生的。消息可能包含实际的页内数据或者指向页外数据的指针。OOL 数据传输是对大量数据传输的一种优化,其中内核将在接收方的虚拟地址空间里为消息分配一个内存区域,而不会创建消息的物理副本。共享内存页将标记 COW。

虚拟内存和内存对象

Mach 的虚拟内存可以清晰的分隔成与机器无关以及与机器关联的部分。例如地址映射,内存对象,共享映射和常驻内存是与机器无关的,而物理映射是与机器相关的。

Mach 的 VM 设计的特性如下:

  • Mach 提供了每个任务的受保护的地址空间,以及稀疏内存布局。任务的地址空间描述是内存区域的线性列表 (vm_map_t),其中每个区域都指向一个内存对象(vm_object_t
  • 机器相关的地址映射包含在 pmap 对象(pmap_t)中
  • 任务可以在自己的地址空间内或者在其他任务的地址空间内分配或取消分配虚拟内存区域
  • 任务可以逐页的指定保护和继承属性

内存

除了基于 Mach 的核心 VM 子系统, Mac OS X 中的内存管理还包含多种其他的机制,严格来讲,其中一些并不是 VM 子系统的一部分,但却与之密切相关。

  • Mach VM 子系统包括机器相关的物理映射模块 (pmap) 以及其他独立于机器的模块,用于管理与各种抽象的对应的数据结构,比如虚拟地址空间映射,VM 对象,命名的实体和驻留的页。内核将把多个例程导出到用户空间,作为 Mach VM API 的一部分
  • 内核使用通用页列表(UPL)数据结构描述一组有界物理页
  • 统一缓冲区缓存(Unified Buffer Cache,UBC)是用于缓存文件的内容以及任务地址空间的匿名部分的页池
  • 内核包括三个内核内部的分页器,默认分页器,设备分页器和虚拟节点分页器。它们在内存区域上处理读入页和写出页操作。分页器使用 UPL 接口以及 Mach 分页器接口的派生接口和 Mach VM 子系统进行通信····
  • ... (与 Mach 无关暂且略过)

Mach VM

Mach VM 特点

  • 清晰地分隔开机器相关的部分与机器无关的部分。只有后者具有完全与 VM 相关的信息
  • 庞大,稀疏的虚拟地址空间 —— 每个任务使用一个虚拟地址空间,并且被该任务内的所有线程完全共享
  • 内存管理与进程间通信的集成。Mach 提供了基于 IPC 的接口,用于处理任务地址空间。这些接口特别灵活,可以允许一个任务操作另一个任务的地址空间
  • 通过对称或非对称写时复制 (Copy-On-Write)算法而进行优化的虚拟复制操作
  • 相关任务与无关任务之间灵活的内存共享,支持写时复制,它在 fork() 和大型 IPC 传输期间是有用的。特别是,在 IPC 消息中,各个任务可以把它们的地址空间的某些部分在相互之间发送
  • 内存映射的文件 mmap
  • 可以通过多个分页器使用的各种后备存储类型

Mach VM 介绍

每个任务的地址空间在内核中都是通过地址映射表示的,这个地址映射就是 VM 映射,它包含内存区域的双向链表以及一个机器相关的物理映射(Pmap)结构。Pmap 处理虚拟-物理地址转换。每个内存区域 —— VM 映射条目 (VM Map Entry) —— 都代表一个连续的虚拟地址范围。不过每个范围都具有它自己的保护和继承基本属性,因此即使地址是有效的,任务也有可能无法为一种或多种操作访问它。而且,VM 映射条目将在列表中按地址进行排序。每个 VM 映射条目都具有一个关联的 VM 对象,它包含关于从其源访问内存的信息。VM 对象包含驻留页或 VM 页的列表。每个 VM 页都是在 VM 对象内通过它距离开始处的偏移量标识的。现在,VM 对象的一些或者全部都可能不是驻留在物理内存中 —— 它可能位于后备存储器(Backing Store)中,例如常规文件,交换文件或硬件设备。VM 对象受内存对象支持,后者在最简单的意义上是一个 Mach 端口,内核可以给它发送消息来获取遗失的数据。内存对象的所有者是内存管理器也叫分页器,是一个特殊的任务,用于给内核提供数据以及在回收时接收修改过的数据。

VM 映射

每个任务的虚拟地址空间都是通过一个 VM 映射数据结构 struct vm_map 描述的。task 结构的 map 字段指向一个 vm_map 结构。

VM 映射是内存区域或 VM 映射条目的集合,其中每个区域都是一组几乎连续的具有相同属性的页。

VM 映射条目

VM 映射条目通过 vm_map_entry 结构表示,由于每个条目都表示当前映射的一个虚拟地址范围,内核将在多个不同时间搜索条目列表,同时还会分配内存。

内核会根据需要拆分或者合并 VM 映射条目。

VM 对象

VM 映射条目是 VM 对象和 VM 映射之间的桥梁,概念上讲,VM 对象时一个连续的数据存储库,其中一些数据可能缓存在驻留内存中,其余的数据可能从相应的后备存储器中获取。负责在物理内存和后备存储器之间传输页的实体就称为分页器 Pager,或者更恰当的讲,是内存管理器。

VM 对象的内容

VM 对象包含其驻留页的大小,以及如何获取非驻留页的信息。驻留页不会再 VM 对象直接共享,即给定的页只会存在于一个 VM 对象内。在销毁某个对象时,需要释放与之关联的所有页,此时附加到 VM 对象上的驻留页结构的列表就特别有用。

VM 对象数据结构包含下面的信息:

  • 对象的大小
  • 指向对象的引用数量
  • 关联的内存对象(分页器)以及进入分页器的偏移量
  • 内存对象控制端口
  • 指向影子对象和副本对象的指针
  • 内核在复制 VM 对象的数据时应该使用的复制策略
  • 一些标识位 (是否是内部对象,临时对象,是否可以持久缓存

内存对象被实现为一个 Mach 端口,分页器拥有对它的接收权限。当内核需要将 VM 对象的页从后备存储器引入物理内存时,它将通过内存对象端口与关联的分页器通信。内核拥有接收权限的内存对象控制端口将用于接收来自分页器的数据。

后备存储器

当数据不是驻留在内存中时,可将其放在后备存储器中。它可以是数据源,但并非一定如此。对于内存映射的文件,后备存储器就是文件自身。当内核需要从物理内存中收回一个受文件支持的页时,它可以简单的丢弃页,除非该页在驻留时已经修改过了,这种情况下将把更改提交给后备存储器。

动态分配的内存(比如 malloc)是匿名的,这是由于它没有作为起点的指定源。在第一次使用匿名内存页时,Mach 将简单地提供一个填充 0 的物理页。特别是,最初没有与匿名内存关联的后备存储器,当内核必须收回这样的页时,它将把交换空间用作后备存储器。匿名内存不会跨系统重新引导而持久存在。对应的 VM 对象也称为内部对象。

分页器

分页器用于操作内存对象和页。它拥有内存对象端口,后者被分页器的客户用作内存对象的页的接口,用于对那些作为接口一部分的页执行读和写操作。内存对象实质上是底层后备存储器的 Mach 端口表示,它代表由内存对象抽象支持的内存范围的非驻留状态。非驻留状态实质上是内核在物理内存中缓存的辅存。

Mac OS X 提供了三种内核中的分页器:

  • 默认分页器:用于在物理内存和交换空间之间传输数据
  • 虚拟节点分页器:用于在物理内存与文件之间传输数据
  • 设备分页器:用于映射专用内存,具有必要的 WIMG 特征

分页器可能提供任意数量的内存对象,其中每个对象都代表分页器管理的一个页范围。相反,任务的地址空间可能具有任意数量的分页器,用于管理它的各个部分。

写时复制 COW

Copy-On-Write 是一种优化技术,其中的内存复制操作将延迟物理页的复制,直到复制操作中涉及的一方写到那个内存。只要是仅仅读取而没有写到复制的数据,写时复制技术将可以同时节省时间和物理内存。甚至当写数据时,也只会复制修改过的页。

Mach 使用一种非对称式写时复制算法,其中源方将保留原始的 VM 对象,内核则会为目标方创建一个新对象。

COW
  • 在执行复制操作时,将创建一个新对象 —— 副本对象(Copy Object),以供目标使用
  • 使副本对象的 shadow 字段指向原始对象
  • 使原始对象的 copy 字段指向副本对象
  • 将副本对象标记为写时复制(并不会把原始对象标记为写时复制)
  • 无论何时将在源映射中修改某一页,都会把它复制到一个新页,并把该页推送给副本对象

物理映射 pmap

VM 映射还指向物理映射 pmap 数据结构,它描述了硬件定义的虚拟-物理地址转换映射。Mach 的 pmap 层封装了机器相关的 VM 代码 —— 特别是,用于管理 MMU 和缓存的 VM 代码,并且可供独立于机器的层使用的通用函数。

驻留内存

Mach 把地址空间分成页,其中页大小通常与原始硬件页大小相同,尽管 Mach 的设计允许从多个物理上连续的硬件页构建更大的虚拟页大小。程序员可见的内存是字节可寻址的,而 Mach 虚拟内存原语只用于操作页。事实上,Mach 将在内部使页对齐内存偏移量,并把内存范围的大小向上取整到最接近的页界限。而且,内核是在页级别执行内存保护的。

vm_page 结构

地址空间的有效部分对应有效的虚拟页。依赖于程序的内存使用模式及其他因素,可以在物理内存中通过驻留页缓存它的一些或者全部虚拟内存,也有可能不缓存任何虚拟内存。vm_page 包含的成员主要有:

  • pageq: 活动/非活动/空闲链表
  • lisq: VM 对象中所有页的列表
  • next: VP 桶链接,指向下一个 vm_page
  • object: 该页所在的 VM d对象
  • offset: 进入 VM 对象的偏移量
  • phys_addr: 物理页编号
  • 其他一些状态位标识

搜索驻留页

内核维护驻留页的散列表,其中 vm_pagenext 字段用于把表中的页连接起来。给定 { VM 对象, 偏移量 } 对,就可以使用散列表查找驻留页。

驻留页队列

可分页的驻留页通过 vm_page 结构的 pageq 字段来标识自己属于哪个驻留页队列。

驻留页队列分为:

  • 空闲队列 Free Queue:包含可立即用于分配的空闲页
  • 非活跃队列 Inactive Queue:包含在任何 pmap 中都没有引用但是仍然具有对象/偏移量页映射的页。这个队列上的页可能是 Dirty Page。当内核需要从某些内存中读出页时,它将从非活动列表中收回驻留页。该列表为 FIFO
  • 活跃队列 Active Queue:包含在至少一个 pmap 中引用的页,也是一个 FIFO 列表,具有类 LRU 排序

页置换

由于物理内存是一种有限的资源,内核必须持续决定应该把哪些页保持为驻留页,应该把哪些页变为驻留页,以及应该从物理内存中回收哪些页。内核使用一种称为带二次机会的先进先出( FIFO with Second Chance )的页置换策略,它近似于 LRU 的行为。

内核使用一组参数来管理上述的三种页队列,这些参数制定了调页阈值及其他约束。页队列管理包括以下特定的操作:

  • 从活跃队列前端把页移到非活跃队列
  • 从非活跃队列中清除 Dirty Page
  • 从非活跃队列中把 Clean Page 移到空闲队列

既然活跃队列是 FIFO,将首先删除最旧的页。如果引用一个非活跃页,将把它移回活跃队列。所以,非活跃队列上的页将有资格第二次被引用。如果某个页足够频繁地被引用,将阻止把它移到空闲队列,从而将不会被回收。

物理内存簿记

调用 vm_page_grab() 将从空闲列表中删除页。如果系统中的空闲页数量小于预留的空闲页数量,这个例程将不会尝试获取页,除非当前线程是一个具有 VM 特权的线程。

页错误

我们知道,在很多情况下虚拟内存映射的物理内存上可能没有指定的数据,这时候会触发页错误 Page Fault 然后才进行页的装载。页错误的触发一般可能有下列原因:

  • 无效访问:地址没有映射到任务的地址空间中。这回导致一个 EXC_BAD_ACCESS Mach 异常,它具有特定的异常代码 KERN_INVALID_ADDRESS,这个异常通常由内核转换成 SIGSEGV 信号
  • 非驻留页:尝试访问当前未进入任务的 pmap 的虚拟页。如果页确实不在物理内存中并且需要从辅助存储器中读取数据(读入页),就会把这些错误归类为“硬”页错误。内核将联系管理请求页的分页器,分页器反过来讲访问关联的后备存储器。不过,如果数据存在于缓存中,它就是一个“软”页错误,在这种情况下,仍然必须在内存中找到页,并且仍然必须建立合适的页转换
  • 违反保护级别:任务尝试访问的页具有比所允许的更高的访问级别。如果违反保护级别是可校正的,内核将透明地处理错误;否则,将把异常报告给错误(通常是作为一个 SIGBUS 信号)。可校正类型的页错误的一个示例是:当任务尝试写到由于 COW 操作而被标记为只读的页时就会发生错误

自举期间的虚拟内存初始化

初始化的 pipeline 如下:

  • vm_mem_bootstrap() 主处理器调用,初始化独立于机器的虚拟内存子系统
  • vm_page_bootstrap() 初始化驻留内存模块
  • zone_bootstrap() 为基于内存区域的内存分配器初始化“区域的区域”
  • vm_object_bootstrap() 初始化 VM 对象模块
  • vm_map_init() 通过建立区域用于分配 VM 映射、VM 映射条目以及仅内核的 VM 映射条目,初始化 VM 映射模块
  • kmem_init() 初始化内核的虚拟内存映射(kernel_map),并且统计此时分配的所有内核内存
  • pmap_init() 初始化 pmap 模块映射虚拟内存所需的其余数据结构,从而完成 pmap 模块的设置
  • zone_init() 创建内核映射的子映射,以便基于区域的内存分配器使用;设置垃圾收集信息
  • kalloc_init() 通过为处理的每个“2 的幂”大小分配一个区域,初始化常规的内核内存 分配器
  • vm_fault_init() 初始化页错误处理模块使用的任何私有数据结构
  • vm_page_module_init() 对驻留内存模块执行额外的设置
  • memory_manager_default_init() 初始化默认内存管理器的全局互斥锁,并将其端口设置为 null (开始时)
  • memory_object_control_bootstrap() 在初始化 VM 对象时,初始化用于分配内存对象请求端口的区域
  • device_pager_bootstrap() 初始化用于分配设备分页器结构的区域

Mach 区域分配器

Mach 区域分配器是一种带有垃圾收集的快速内存分配机制。区域是可以通过一个用于分配和取消分配的高效接口访问的固定大小的内存块的集合。内核通常会为每一种要管理的数据结构类别创建一个区域。Mac OS X 内核会为其创建各个区域的数据结构的示例如下:

  • 异步 I/O 工作队列条目
  • 警报和定时器数据
  • 内核审计记录
  • 内核通知
  • 任务,线程和用户线程
  • 管道
  • 信号
  • 缓冲区头部和元数据缓冲区
  • 网络栈中的多个协议控制块
  • 统一缓冲区缓存 'info' 结构
  • 虚拟结点页和设备分页器
  • Mach VM 数据结构,比如 VM 映射,映射条目,副本对象,VM 对象散列条目和页
  • Mach IPC 数据结构,比如 IPC 空间,IPC 树条目,端口,端口集和 IPC 消息

IPC

Mac OS X 提供了大量 IPC 机制,其中一些具有在系统的多个层中可用的接口。下面是 Mac OS X 中的 IPC 机制/接口的示例:

  • Mach IPC —— 最低级的 IPC 机制,是许多高层机制的基础
  • Mach 异常
  • UNIX 信号
  • 无名管道
  • 命名管道
  • XSI/System V IPC
  • POSIX IPC
  • Distributed Objects
  • Apple Events
  • 多个用于发送和接收通知的接口,比如 notifykqueue
  • Core Foundation IPC 机制

Mach IPC 简介

Mach 提供了一种面向消息的 IPC 设施,它代表了由 Mach 的先驱使用的类似方法的演化。Mach 的 IPC 实现使用 VM 子系统,利用 COW 优化有效地传输大量数据。Mac OS X 内核使用由 Mach 的 IPC 接口提供的通用消息原语作为低级构件。特别是,Mach 的 mach_msg()mach_msg_overwrite() 调用可用于发送和接收消息,从而允许将 RPC 风格的交互作为 IPC 的特例。这类 RPC 可用于在 Mac OS X 中实现多种系统服务。

Mach IPC 设施构建与两个基本的内核抽象之上,它们是端口消息,其中把在端口之间传递消息作为基本的通信机制。端口是一个多面实体,而消息则是数据对象的任意大小的集合。

Mach 端口

Mach 端口在操作系统中服务于以下目的:

  • 端口是一个通信信道 —— 一个内核保护,内核管理,长度有限制的消息队列。端口上的最基本的操作是用于发送(Send)和接收(Receive)消息。发送到端口允许任务把消息放在端口的底层队列中。接收消息允许任务从该队列中获取消息,这个队列将保存进入的消息,直到接收方移除它们为止。当与端口对应的队列填满或者清空时,一般而言将分别阻塞发送方和接收方
  • 端口用于表示能力,这是由于它们自身受一种能力机制保护,以便组织任意的 Mach 任务访问它们。要访问某个端口,任务必须具有端口能力或端口权限,比如发送权限(Send Right)或接收权限(Receive Right)。任务具有的对端口的特定权限限制了任务可能在该端口上执行的操作集。这允许 Mach 阻止未经授权的任务访问端口,尤其是阻止任务操作与端口关联的对象
  • 端口用于表示资源、服务和设施,从而提供对这些抽象的对象风格的访问。例如,Mach 使用端口表示诸如主机、任务、线程、内存对象、时钟、定时器、处理器和处理器之类的抽象。在这类端口表示的对象上的操作时通过给它们的代表端口发送消息来执行的。内核通常持有对此类端口的接收权限,它将接收和处理消息。这类似于面向对象方法调用

端口的名称(name)可以代表多个实体,比如用于发送或接收消息的实体、死名、端口集,或者不代表任何事物。一般而言,可以称端口名称代表的是端口权限。

端口用于通信

在端口作为通信信道的角色时,Mach 端口类似于 BSD 套接字,但是具有一些重要的区别,比如:

  • 根据设计,Mach IPC 是与虚拟内存子系统集成在一起的
  • 套接字主要用于远程通信,而 Mach IPC 则主要用于机器内部的通信
  • Mach IPC 消息可以携带类型化的内容
  • 一般而言,Mach IPC 接口比套接字接口更强大、更灵活

端口权限

  • MACH_PORT_RIGHT_SEND:端口发送权限
  • MACH_PORT_RIGHT_RECEIVE:端口接收权限
  • MACH_PORT_RIGHT_SEND_ONCE:发送一次的权限
  • MACH_PORT_RIGHT_PORT_SET:可以将端口集名称视作包含多个端口的接收权限
  • MACH_PORT_RIGHT_DEAD_NAME:死名,实际上不是一种权限,它代表由于对应的端口被销毁而变成无效的发送权限或发送一次的权限

端口作为对象

Mach IPC 设施是一种通用的对象引用机制,它把端口用作受保护的接入点。从语义上讲,Mach 内核是一个服务器,为多个端口上的对象提供服务。这个内核服务器将接收进入的消息,通过执行所请求的操作来处理它们,并且如果需要,还会发送应答。

Mach IPC 消息

可以通过 mach_msg 函数家族发送和接收 Mach IPC 消息。Mac OS X 中的基本 IPC 系统调用是一个名为 mach_msg_overwrite_trap() 的陷阱,它可用于在单个调用中发送消息、接收消息或者既发送又接收。

Mach 消息的成分随着时间的推移在不断演化,但是由固定大小的头部和其他可变大小的数据组成的基本布局一直保持不变。Mach OS X 中的 Mach 消息包含以下部分:

  • 固定大小的消息头部(mach_msg_header_t
  • 可变大小(可能为空)的消息主体,其中包含内核和用户数据(mach_msg_body_t
  • 可变大小的尾部——多种类型之一,其中包含由内核追加的消息基本属性(mach_msg_trailer_t),尾部只与接收方相关

Mach IPC:Mac OS X 实现

IPC 空间

每个任务都具有私有的 IPC 空间,用于端口的命名空间,在内核中通过 ipc_space 结构表示。任务的 IPC 空间定义了它的 IPC 能力。因此,诸如发送和接收之类的 IPC 操作将会查询这个地址空间。类似地,用于操控任务权限的 IPC 操作将会在任务的 IPC 空间上工作。

里面主要会包含 IPC 条目表以及 IPC 条目伸展树。

端口的构成

Mach 端口在内核中通过一个指向 ipc_port 结构的指针表示。IPC 条目结构的 ipc_object 字段指向一个 ipc_object 结构,它在逻辑上叠加在 ipc_port 结构之上。

ipc_port 结构的字段包括:一个指向持有接收权限的任务的 IPC 空间的指针、一个指向端口表示的内核对象的指针,以及多个引用计数,比如执行发送计数、发送权限的数量以及发送一次权限的数量。

任务(task)和 IPC

Mach 任务和线程在开始其寿命时都具有某些标准的端口集,除了任务的标准端口之外,task 结构还包含一个指向任务的 IPC 空间的指针(itk_space)。

线程和 IPC

与任务类似,线程包含一个自身端口以及一组用于错误处理的异常端口。新创建的任务的异常端口是从父任务继承的,而线程的每个异常端口则会在创建线程时初始化为空端口。任务和线程的异常端口都可以在以后以编程的方式改变。如果用于某种异常类型的线程异常端口是空端口,内核将使用下一个更明确的端口:对应的任务级异常端口。

消息传递实现

Mach 消息传递是可靠的而且会保序。因此消息可能不会丢失,并且总会以发送它们的顺序进行接收。不过,内核在递送那些发送给发送一次权限的消息时总是乱序的,并且不会考虑接收端口的队列长度或者它是如何填满的。如前所述,端口的消息队列的长度是有限的。当队列填满时,可能会发生下列行为:

  • 默认行为是阻塞新发送方,直到队列中有空间为止
  • 如果发送方在调用 mach_msg()mach_msg_overwrite() 时使用 MACH_SEND_TIMEOUT 选项,那么最多会将发送方阻塞指定的时间
  • 如果消息是使用发送一次权限发送的,无论队列是否填满,内核都将递送消息

Mach 异常

异常是由程序自身引发的对正常的程序控制流程的同步中断。下面列出了常见的异常原因示例:

  • 尝试访问不存在的内存
  • 尝试访问违反地址空间保护的内存
  • 由于非法或未定义的操作码或操作数而导致无法执行的指令
  • 产生算术错误,比如除以 0,上溢或下溢
  • 执行打算用于支持仿真的指令
  • 碰到调试器安装的断点或者与调试、跟踪和错误检测相关的其他异常
  • 执行系统调用指令

Mach 提供了一种基于 IPC 的异常处理设施,其中将把异常转换成消息。当异常发生时,将把包含关于异常信息(比如异常类型、引发它的线程以及线程的包含任务)的消息发送到异常端口。对该消息的应答(线程将等待它)指示异常是否被异常处理程序成功处理。在 Mach 中,异常时系统级原语。

异常端口集(每种异常一个端口)是在主机、任务和线程级别维护的。当要递送异常消息时,内核首先将尝试把它递送到最明确的端口。因此,顺序是线程,任务和主机。

Mach 异常处理程序是异常消息的接收方。它在其自己的线程中运行。线程如果希望成为任务的异常处理程序,它可以调用 task_set_exception_ports() ,将它的端口之一注册为任务的异常端口之一,单个端口可用于接收多种类型的异常消息。