Mach消息发送机制

目录

  • Mach基础
  • Mach作用
  • Mach消息
    • 简单消息
    • 复杂消息
    • 端口
  • 消息传递实现

Mach基础

Mach是iOS的XNU内核中最为核心的部分,称为核心中的核心。

Mach中,所有东西都是通过自己的对象实现的。进程、线程和虚拟内存都是对象,所有的对象都有自己的属性。Mach采用消息传递的方式实现对象与对象之间的通信。Mach对象不能直接调用另一个对象,只能传递消息,源对象发送一条消息,这条消息加入到目标对象的队列中等待处理。如果产生应答,则通过另一条消息传递回去,消息遵从FIFO方式。

Mach作用

Mach作为核心中的核心,设计之初是为了把一些不重要的功能移到用户态,因此Mach中留下的功能都是内核最重要的功能。

  • 线程管理
  • 线程资源分配
  • 虚拟内存分配及管理
  • 底层物理资源的分配

Mach消息

上文说到Mach中对象之间通信的方式是消息传递。消息在Mach处于很重要的地位,是MachIPC的核心构建块。消息定义在<mach/message.h>文件中,在xcode中就能够看到。

简单消息

一个简单消息由三个部分组成:

  • 一个强制要有的消息头(mach_msg_header_t)
  • 一个可选的body(mach_msg_body_t)
  • 一个可选的tailer(mach_msg_trailer_t),这个tailer只与接收端有关系。

结构如下所示

bits是标志位,表示消息的性质。size则表示消息的大小。id标识了该消息的唯一性。

复杂消息

复杂消息是带有一些额外字段和结构的消息。结构如下图:

可以看出,相比于简单消息,它的结构要比简单消息多一段“附件”,也就是body。

typedef struct
{
    mach_msg_size_t msgh_descriptor_count;
} mach_msg_body_t;

复杂消息靠msgh_bit(标志位)来标识,当msgh_bit设置为MACH_MSGH_BITS_COMPLEX时,表示该消息为复杂消息。复杂消息与简单消息结构差异处在于header后面接着一个描述符计数字段(msgh_descrptor_count),然后是一个接一个的串行化的描述符,最后才是data。之所以前面说复杂消息带有"附件",附件就是mach_msg_type_descriptor_t,在message.h可以看到它的定义

typedef struct
{
  natural_t         pad1;
  mach_msg_size_t       pad2;
  unsigned int          pad3 : 24;
  mach_msg_descriptor_type_t    type : 8;
} mach_msg_type_descriptor_t;

第一个参数相当于"附件",第二个参数是附件的大小,第四个参数代表了附件的类型。定义如下:

typedef unsigned int mach_msg_descriptor_type_t;

#define MACH_MSG_PORT_DESCRIPTOR        0
#define MACH_MSG_OOL_DESCRIPTOR         1
#define MACH_MSG_OOL_PORTS_DESCRIPTOR       2
#define MACH_MSG_OOL_VOLATILE_DESCRIPTOR    3
type 用途
MACH_MSG_PORT_DESCRIPTOR 传递一个端口权限
MACH_MSG_OOL_DESCRIPTOR 传递out-of-line数据
MACH_MSG_OOL_PORTS_DESCRIPTOR 传递out-of-line端口
MACH_MSG_OOL_VOLATILE_DESCRIPTOR 传递有可能变化的out-of-line数据

这里要说明的是mach_msg_type_descriptor_t只是相当于一个基类,Mach还提供了其他更加具体的结构体供我们使用。例如:

typedef 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;//在address处数据的大小
} mach_msg_ool_descriptor64_t;

这是64位out-of-line数据可以使用的"附件包",message.h中还有其他结构体,这里就不再赘述了。

前面说了那么多out-of-line,还没说out-of-line是什么。out-of-line是Mach消息的一项重要特性,允许添加指向各种数据的分散指针,就像附件一样。简单来说,OOL描述符描述了要附加的数据的地址的大小以及如何处理数据的指令,还有复制选项。常用于传递大块数据,并且能够避免进行昂贵的复制操作。

端口

端口是一个32位的整型标识符,消息在端口之间传递。消息从某一个端口发送到另一个端口,每一个端口都可以接收来自任意发送者的消息,但是每一个消息只能有一个接收者。向一个端口发送消息实际上是将消息放在队列中,直到消息被处理。

所有的Mach原生对象都是通过端口访问的,换句话说,我们要查找一个对象的句柄(标识应用程序中的不同对象和同类中的不同的实例的值),实际上查找的是这个对象端口的句柄。

消息传递实现

用户态的Mach消息传递使用的是Mach_msg()函数,这个函数通过内核的Mach陷阱把自己从用户态陷入内核态。函数原型如下:

mach_msg_return_t   mach_msg(
                    mach_msg_header_t *msg,
                    mach_msg_option_t option,
                    mach_msg_size_t send_size,
                    mach_msg_size_t rcv_size,
                    mach_port_name_t rcv_name,
                    mach_msg_timeout_t timeout,
                    mach_port_name_t notify);

无论对于发送还是接收,使用的都是Mach_msg()。

  1. 发送消息

    发送消息的步骤如下所示:

    • 调用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队列中并唤醒等待的线程。
  2. 接收消息

    接受消息的步骤如下所示:

    • 调用current_space()获取当前的IPC空间。
    • 调用current_map()获取虚拟空间
    • 调用ipc_mqueue_copyin()获取IPC队列。
    • 调用ipc_mqueue_receive()从队列中取出消息
    • 执行

推荐阅读更多精彩内容