Mach
OS X内核的基本服务和基本类型都基于Mach 3.0。苹果对Mach进行了修改和扩展,以更好地满足OS X的功能和性能目标。
Mach 3.0最初被认为是一个简单、可扩展的通信微内核。它能够作为一个独立的内核运行,而其他传统的操作系统服务(如I/O、文件系统和网络堆栈)作为用户模式服务器运行。
然而,在OS X中,Mach与其他内核组件链接到一个内核地址空间
中。这主要是为了性能
;在链接组件之间进行直接调用要比在单独任务之间发送消息
或执行远程过程调用(RPC)
快得多。这种模块化的结构导致了一个比单一内核所允许的更健壮和可扩展的系统,而没有纯微内核的性能损失。
因此,在OS X中,Mach主要不是客户机和服务器之间的通信中心。相反,它的价值由抽象、可扩展性和灵活性组成。特别地,Mach提供了
- 基于对象的api,使用通信通道(例如端口)作为对象引用
- 高度并行执行,包括预先调度的线程和对SMP的支持
- 一个灵活的调度框架,支持实时使用
- 一组完整的IPC原语,包括消息传递、RPC、同步和通知
- 支持大型虚拟地址空间、共享内存区域和由持久性存储支持的内存对象
- 经过验证的可扩展性和可移植性,例如跨指令集体系结构和分布式环境
- 安全与资源管理作为设计的基本原则;所有资源都是虚拟化的
Mach内核抽象化
Mach提供了一组被设计成既简单又强大的抽象。这些是主要的内核抽象:
- 任务(Tasks)。资源所有权单位;每个任务由一个虚拟地址空间、一个端口名称空间和一个或多个线程组成。(类似于流程。)
- 线程(Threads)。任务中CPU执行的单位。
- 地址空间(Address space)。与内存管理器一起,Mach实现了稀疏虚拟地址空间和共享内存的概念。
- 内存对象(Memory objects)。内存管理的内部单元。内存对象包括命名项和区域;它们表示可能映射到地址空间的持久数据。
- 端口(Ports)。安全、简单的通信通道,只能通过发送和接收功能(称为端口权限)访问。
- IPC。消息队列、远程过程调用、通知、信号量和锁集。
- 时间(Time)。时钟、定时器和等待。
在陷阱级别,大多数Mach抽象的接口由表示这些对象的内核端口之间
的消息
组成。陷阱(trap)级别的接口
(例如mach_msg_overwrite_trap)和消息格式
在正常使用中由Mach接口生成器(MIG)抽象出来。MIG用于编译基于消息的api的过程接口
,基于这些api的描述
。(MIG参看下面总结部分)
任务和线程
OS X进程和POSIX线程(pthreads)分别在Mach任务和线程之上实现。线程是任务中的控制流点。存在一个任务来为其包含的线程提供资源。这种分割是为了提供并行性和资源共享。
一个线程
- 是任务中的控制流点。
- 可以访问包含任务的所有元素。
- 与其他线程(可能)并行执行,甚至是同一任务中的线程。
- 具有最小的状态信息,以降低开销。
一个任务
- 是系统资源的集合。这些资源(地址空间除外)由端口引用。如果对端口被分配了权限,那么这些资源可以与其他任务共享。
- 提供一个大的、可能稀疏的地址空间,由虚拟地址引用。这个空间的一部分可以通过继承或外部内存管理共享。
- 包含一些线程。
请注意,任务没有自己的线程执行指令的生命周期。当说“task Y做X”时,真正的意思是“task Y中包含的线程做X”。
任务是一个相当昂贵的实体。它的存在是资源的集合。任务中的所有线程都共享所有内容。如果没有显式的操作(尽管操作通常很简单),两个任务就不能共享任何内容,而且一些资源(例如端口接收权限)根本不能在两个任务之间共享。
线程是一个相当轻量级的实体。它的创建成本相当低,操作开销也很低。这是真的,因为一个线程只有很少的状态信息(主要是它的寄存器状态)。它所拥有的任务承担着资源管理的重担。在多处理器计算机上,任务中的多个线程可以并行执行。即使并行性不是目标,多线程也有一个优势,即每个线程都可以使用同步编程风格,而不是试图使用单个线程进行异步编程来提供多个服务。
线程是基本的计算实体。一个线程只属于一个任务,这个任务定义了它的虚拟地址空间。影响地址空间的结构或引用任何资源以外的地址空间,线程必须执行一个
特殊的陷阱指令
引起内核代表线程
执行操作或发送消息代理
代表线程。通常,这些陷阱操作与包含线程的任务相关的资源。内核可以发出请求来操作这些实体:创建它们、删除它们并影响它们的状态。Mach为线程调度策略提供了一个灵活的框架。OS X的早期版本同时支持分时和固定优先级策略。提高和降低分时线程的优先级,以平衡它与其他分时线程之间的资源消耗。
固定优先级的线程执行一定的时间量,然后放在具有相同优先级的线程队列的末尾。将固定优先级线程的量子级别设置为无穷大,可以让线程一直运行,直到阻塞,或者直到被优先级更高的线程抢占为止。
高优先级实时线程
通常是固定优先级
的。OS X还为实时性能提供了时间约束调度。这个调度允许您指定线程必须在一定时间内获得一定的时间量。
Mach调度在Mach调度和线程接口中有进一步的描述。
端口、端口权限、端口集和端口名称空间
除了任务的虚拟地址空间之外,所有其他Mach资源都是通过称为端口的间接级别访问的。端口是请求服务的客户机和提供服务的服务器之间单向通信通道的端点。如果要向此类服务请求提供应答,则必须使用第二个端口。这类似于UNIX中的(单向)管道。
在大多数情况下,由端口访问的资源(即由端口命名的资源)被称为对象
。大多数由端口命名的对象都有一个接收方
和(可能的)多个发送方
。也就是说,对于典型对象(如消息队列),只有一个接收端口,至少有一个发送端口。
对象提供的服务由接收发送到对象的请求的管理器决定。因此,内核是与内核提供的对象关联的端口的接收方,而与任务提供的对象关联的端口的接收方是提供这些对象的任务。
对于命名任务提供的对象的端口,可以将该端口的请求接收方更改为不同的任务,例如通过在消息中将该端口传递给该任务。一个任务可能有多个引用其支持的资源的端口。就此而言,任何给定的实体都可以有多个表示它的端口,每个端口表示不同的允许操作集。例如,许多对象都有一个名称端口和一个控制端口(有时称为特权端口)。对控制端口的访问允许操作对象;对name端口的访问简单地为对象命名,这样您就可以获得关于它的信息,或者对它执行其他非特权操作。
任务具有以特定方式访问端口的权限(发送、接收、发送一次);这些被称为port rights。端口只能通过右值访问。端口通常用于授予客户机对Mach内对象的访问权。有权发送到对象的IPC端口表示有权以规定的方式操作对象。因此,端口所有权是Mach内部的基本安全机制
。拥有访问对象的权利
就是拥有访问或操作该对象的能力
。
端口权限可以通过IPC在任务之间复制和移动
。这样做实际上是将功能传递给某个对象或服务器。
一种类型的对象引用一个端口是一个端口组。顾名思义,端口设置一组端口的权利时,可以当作一个单独的单元接收消息或事件的任何成员集。港口集允许一个线程等待的消息和事件源,例如在工作循环。
传统上,在Mach中,由端口表示的通信通道总是消息队列。然而,OS X支持其他类型的通信通道,这些新的IPC对象类型也由端口和端口权限表示。有关消息和其他IPC类型的详细信息,请参阅下面进程间通信(Interprocess Communication, IPC)一节。
端口和端口权限没有允许直接操作任意端口或权限的系统范围名称。只有当任务在其端口名称空间中具有端口权时,才可以由任务操作端口。端口权由端口名称指定,一个整数索引进入一个32位端口名称空间。每个任务都与它关联一个端口名称空间。
当另一个任务显式地将它们插入它的名称空间时,当它们在消息中接收到权限时,任务通过创建返回对象权限的对象获得端口权限,并通过Mach调用特定的特殊端口(mach_thread_self、mach_task_self和mach_reply_port)获得端口权限。
内存管理
与大多数现代操作系统一样,Mach提供对大型、稀疏的虚拟地址空间的寻址。运行时访问是通过虚拟地址进行的,这些虚拟地址可能与初始访问时物理内存中的位置不对应。Mach负责获取一个请求的虚拟地址,并在物理内存中为它分配一个相应的位置。它通过请求分页来实现这一点。
当内存对象映射到虚拟地址空间的范围时,将用数据填充虚拟地址空间的范围。地址空间中的所有数据最终都是通过内存对象提供的。Mach在物理内存中建立页面时,向内存对象(分页器)的所有者询问页面的内容,并在回收页面之前将可能修改过的数据返回给分页器。OS X包含两个内置分页器——默认分页器和vnode分页器(default pager and the vnode pager)。
默认的分页器处理非持久性内存,称为匿名内存。匿名内存是零初始化的,并且只在任务执行期间存在。vnode分页器将文件映射到内存对象。Mach向内存对象导出一个接口,允许用户模式任务提供内存对象的内容。这个接口称为外部内存管理接口(EMMI)。
内存管理子系统导出称为命名项或命名内存项的虚拟内存句柄。与大多数内核资源一样,这些资源由端口表示。拥有一个命名的内存条目句柄,允许所有者映射底层虚拟内存对象,或者将映射底层对象的权利传递给其他人。在两个不同的任务中映射命名项会在两个任务之间生成共享内存窗口,从而为建立共享内存提供了一种灵活的方法。
从OS X v10.1开始,EMMI系统得到了增强,以支持“无端口”EMMI。在传统的EMMI中,为每个内存区域创建两个Mach端口,同样也为每个缓存的vnode创建两个端口。在最初的实现中,无端口的EMMI用直接内存引用(基本上是指针)替换了这一点。在将来的版本中,端口将用于与内核外部的分页器通信,同时使用直接引用与驻留在内核空间中的分页器通信。这些更改的最终结果是,早期版本的无端口EMMI不支持运行在内核空间之外的分页器。这种支持有望在未来的版本中恢复。
虚拟内存空间的地址范围也可以通过直接分配(使用vm_allocation)来填充。底层虚拟内存对象是匿名的,并由默认分页程序支持。地址空间的共享范围也可以通过继承设置。创建新任务时,将从父任务中克隆它们。这种克隆也属于底层内存地址空间。根据与映射相关的属性,对象的映射部分可以作为副本继承,也可以作为共享,或者根本不可以。Mach采用一种称为“写中复制”的延迟复制形式,以优化任务创建时继承副本的性能。
不是直接复制范围,而是通过受保护的共享来实现写时复制优化。这两个任务共享要复制的内存,但具有只读访问。当任何一个任务试图修改范围的一部分时,该部分将被复制。这种对内存副本的延迟计算是一种重要的优化,它允许在几个方面进行简化,尤其是消息传递api。
Mach还提供了另一种形式的共享,通过导出指定的区域。命名区域是命名条目的一种形式,但是它不是由虚拟内存对象支持的,而是由虚拟映射片段支持的。这个片段可能包含到许多虚拟内存对象的映射。它可以映射到其他虚拟映射,提供了一种方法,不仅可以继承一组虚拟内存对象,还可以继承它们现有的映射关系。该特性在任务设置中提供了重要的优化,例如在共享用于共享库的地址空间的复杂区域时。
进程间通信(IPC)
任务之间的通信是Mach哲学的一个重要元素。Mach支持客户机/服务器系统结构,其中任务(客户机)通过通过通信通道发送的消息请求其他任务(服务器)来访问服务。
Mach中这些通信通道的端点称为端口,而端口权限表示使用该通道的权限。Mach提供的IPC形式包括
- 消息队列
- 信号量
- 通知
- 锁集
- 远程过程调用(rpc)
由端口表示的IPC对象的类型决定了该端口上允许的操作,以及数据传输的方式(和是否)。
重要提示:OS X中的IPC设施处于过渡状态。在系统的早期版本中,并不是所有这些IPC类型都可以实现。
对于端口的原始操作,有两种基本不同的Mach api, mach_ipc家族和mach_msg家族。在合理的范围内,这两个系列都可以用于任何IPC对象;然而,在新代码中,mach_ipc调用是首选的。mach_ipc调用在适当的地方维护状态信息,以便支持事务的概念。mach_msg调用支持遗留代码,但不推荐使用;他们是无状态的。
IPC事务和事件调度
当线程调用mach_ipc_dispatch,它反复处理事件设置的注册端口。这些事件可以从RPC参数块对象(如客户调用的结果),一个锁对象被(由于其他线程释放锁),通知或信号量被发布或消息来自一个传统的消息队列。
这些事件通过mach_msg_dispatch的调用来处理。有些事件暗示调用生命周期内存在事务。在锁的情况下,状态是锁的所有权。当callout返回时,锁被释放。在远程过程调用的情况下,状态是客户机的标识、参数块和应答端口。当callout返回时,将发送应答。
当callout返回时,事务(如果有的话)就完成了,线程等待下一个事件。mach_ipc_dispatch工具旨在支持工作循环。
消息队列
最初,Mach中进程间通信的唯一样式是消息队列。只有一个任务可以持有表示消息队列的端口的接收权。这个任务允许从端口队列接收(读取)消息。多个任务可以拥有向队列发送(写)消息的端口的权限。
任务通过构建包含一组数据元素的数据结构与另一个任务通信,然后在其拥有发送权限的端口上执行消息发送操作。稍后,具有该端口接收权限的任务将执行消息接收操作。
一条消息可包括以下部分或全部:
- 纯数据
- 存储范围的副本
- 端口的权利
- 内核隐式属性,如发送方的安全令牌
消息传输是一个异步操作。消息逻辑上被复制到接收任务中,可能使用写时复制优化。接收任务中的多个线程可以尝试从给定端口接收消息,但是只有一个线程可以接收任何给定的消息。
信号量
信号量IPC对象支持等待、post和post所有操作。这些是计数信号量,如果在该信号量的等待队列中当前没有线程在等待,则保存(计数)post。post all操作将唤醒所有当前等待的线程。
通知
与信号量一样,通知对象也支持post和wait操作,但添加了一个state字段。状态是一个固定大小、固定格式的字段,在创建通知对象时定义。每个post更新状态字段;每个post都覆盖一个状态。
锁
锁是提供对临界区互斥访问的对象。锁的主要接口是面向事务的(参见IPC事务和事件调度)。在事务期间,线程持有锁。当它从事务返回时,锁被释放。
远程过程调用(RPC)对象
顾名思义,RPC对象旨在促进和优化远程过程调用。RPC对象的主要接口是面向事务的(参见IPC事务和事件调度)
当创建RPC对象时,定义一组参数块格式。当客户机发出RPC(对象上的发送)时,它会导致以预定义格式之一的消息创建并在对象上排队,然后最终传递给服务器(接收方)。当服务器从事务返回时,将回复返回给发送方。Mach试图通过使用客户机的资源执行服务器来优化事务;这称为线程迁移
。
时间管理
Mach中时间的传统抽象是时钟,它提供了一组基于mach_timspec_t的异步报警服务。有一个或多个时钟对象,每个对象定义一个以纳秒为单位的单调递增的时间值。实时时钟是内置的,是最重要的,但是系统中可能还有其他时钟用于其他时间概念。时钟支持获取当前时间、给定时间段的睡眠、设置闹钟(在给定时间发送的通知)等操作。
mach_timespec_t API在OS x中是不推荐的。更新的、首选的API基于计时器对象,而计时器对象又使用AbsoluteTime作为基本数据类型。AbsoluteTime是一种依赖于机器的类型,通常基于平台本机时基。提供了一些例程来将绝对时间值转换为其他数据类型,或者从其他数据类型转换为绝对时间值,比如纳秒。计时器对象支持异步、无漂移通知、取消和提前警报。它们比时钟效率更高,分辨率更高。
我们总结一下
Mach 、端口、Mach消息部分
- 在Mach中所有东西(Task、线程、虚拟内存等)都是对象
- 对象与对象之间通信只能(任务虚拟地址空间)通过
端口
收发(一个接收方,可以多个发送方
)消息。
Mach做以下几件事儿:
- “控制点”或执行单元的管理。
- 线程或线程组(Task)的资源分配。
- 虚拟内存的分配和管理。
- 底层物理资源–即CPU、内存和任何物理设备的分配。
Mach消息结构体
- 最基本的包含两部分:消息头、消息体。可选的消息尾
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24typedef struct
{
mach_msg_bits_t msgh_bits;//标志位
mach_msg_size_t msgh_size;//大小
mach_port_t msgh_remote_port;//目标端口(发送:接受方,接收:发送方)
mach_port_t msgh_local_port; //源端口(发送:发送方,接收:接收方)
mach_port_name_t msgh_voucher_port;
mach_msg_id_t msgh_id;
} mach_msg_header_t; //消息头
typedef struct
{
mach_msg_size_t msgh_descriptor_count;
} mach_msg_body_t;//消息体
typedef struct
{
mach_msg_header_t header;
mach_msg_body_t body;
} mach_msg_base_t; //基本消息
typedef unsigned int mach_msg_trailer_type_t;//消息尾的类型
typedef struct
{
mach_msg_trailer_type_t msgh_trailer_type;
mach_msg_trailer_size_t msgh_trailer_size;
} mach_msg_trailer_t; //消息尾 - 复杂一点的,将消息头的标志位mach_msg_bits_t设置为MACH_MSGH_BITS_COMPLEX,就表示复杂消息。此时消息体里面指定了描述符的个数,接下来就是一个接着一个的描述符:
1
2
3
4
5
6
7
8
9
10typedef struct
{
uint64_t address;//数据的大小
boolean_t deallocate: 8;//发送之后是否接触分配
mach_msg_copy_options_t copy: 8;//复制指令
unsigned int pad1: 8;
mach_msg_descriptor_type_t type: 8;
mach_msg_size_t size;//数据的大小
} mach_msg_ool_descriptor64_t;
消息收发
1 | extern mach_msg_return_t mach_msg( |
步骤如下:
发送消息:
- 调用current_space()获取当前的IPC空间。
- 调用current_map()获取虚拟空间
- 消息大小正确性检查
- 计算要分配的消息大小
- 通过ipc_kmsg_alloc分配消息
- 复制消息
- 复制消息关联的端口权限,然后通过ipc_kmsg_copyin将所有的out-of-line数据的内存复制到当前虚拟空间。(如果不复制权限可能导致无法访问数据)
- 调用ipc_kmsg_send()发送消息
- 获得msgh_remote_port引用并锁定端口
- 调用ipc_mqueue_send(),将消息直接复制到端口的ipc_messages队列中并唤醒等待的线程。
接收消息
- 调用current_space()获取当前的IPC空间。
- 调用current_map()获取虚拟空间
- 调用ipc_mqueue_copyin()获取IPC队列。
- 调用ipc_mqueue_receive()从队列中取出消息
- 执行
端口
端口实际上就是一个整型的标识符,是如下结构(在osfmk/ipc/ipc_port.h中定义)的一个句柄:
1 | struct ipc_port { |
Mach接口生成器 MIG
Mach消息传递模型是远程调用(Remote Procedure Call,RPC)的一种现实(类似Thrift)。在/usr/include/mach目录下可以看到一些.defs文件,这些文件包含了Mach子系统(一组操作)的定义。操作类型如下:
IPC
- Mach的每个Task都包含一个指针,这个指针指向一个IPC命名空间,这个IPC命名空间了包含了Task的端口,当然Task还可以获取系统范围的端口,例如:主机端口、特权端口(可以重启机器等)等。
- 在用户态下,消息传递都是通过mach_msg()函数实现的,这个函数会触发一个mach陷阱mach_msg_trap(),接下来mach_msg_trap()又会调用mach_msg_overwrite_trap(),它会通过MACH_SEND_MSG和MACH_RCV_MSG来判断是发送操作,还是接收操作。
- 期中内核态中还可以通过mach_msg_receive()和mach_msg_send()来收发数据。
主机、时钟、处理器、处理器集
主机对象 Host
主机就是一组“特殊”端口的集合,以及一组异常处理程序的集合,同时定义了一个锁用于保护异常处理的并发访问。结构如下:
1 | struct host { |
时钟对象(Clock)
Mach内核提供了一个简单的“时钟”对象(在osfmk/kern/clock.h中定义)的抽象,这个对象用于计时和闹铃,期中最重要的内部API是clock_deadline_for_periodic_event(),调度器通过它设置了一个重复发生的通知–从而保证了多任务引擎的运转。
处理器对象(Processer)
在多核架构中每一个核心都可以看做是一个CPU,处理器被分配给处理器集,处理器是CPU的简单抽象,被Mach用于一些基本的操作,比如:启动和关闭一个CPU,向CPU分发要执行的线程。结构的定义(在osfmk/kern/processor.h)如下:
1 | struct processor { |
其中最重要的是runq,这是分发到这个处理器的线程队列。
处理器集
处理器集就是一个或多个processor_t的分组,也被称为pset。pset通常维护三个队列:
active_queue:用于保存当前正在执行线程的CPU。
idle_queue:用于保存当前空闲的CPU(例如:正在执行idle_thread)。
pset_runq:保存了在这个集合中的所有CPU上执行的线程。
processor_set的定义如下:
1 | struct processor_set { |