MacOSX Mach IPC 入门

一、简介

Mach,是一个面向通信的操作系统微内核,其基本工作单位为 task(而不是 process)。Mach 内核提供了一种 IPC 机制,而 XNU 的大多数服务也建立在 Mach IPC 和 Mach Task 上。

Mach 有多种抽象的基本概念,其中一部分分别是 taskthreadportmessagememory object

Mach 微内核作为 MacOS XNU 内核的组成部分,接管了相当重要的一部分功能。其中最著名的莫过于 Mach IPC 进程间通信机制。

本人将在这里简单记录一下 Mach IPC 部分机理。

需要注意的是,这是本人第一次接触 Mach IPC,因此其中可能会有一部分陈述或者说明存在问题,还请各位师傅不吝指出。

二、Mach Task & Thread

Mach 将传统的 UNIX 进程抽象拆分成了 taskthread。其中:

  • task 是一个执行环境与静态实体。它并不直接执行计算,而是提供了一个框架,其他实体(例如线程)在其中执行。内核中的BSD 进程(类似 Unix 进程)与 Mach task 有着一一对应的关系。

    task 还是资源分配的基本单元。那些与 BSD 进程所关联的资源被包含于 task 中。

    同时每个 task 也代表了保护边界。在获取访问权限前,不同 task 不能访问其他的 task 中的资源。

  • thread 是 Mach 中实际执行的实体,也是 task 的控制流执行点。它在 task 的上下文中执行。

    thread 执行的代码驻留在其 task 中的地址空间中。每个 task 中包含 0 至多个 thread。

通过上面的说明,我们也可以将 task 这个概念,间接理解成传统意义上的 process(是不是非常的相似:))

需要注意的是:一旦创建了 task,那么任何持有着 task identifier 的用户都可以修改 task

三、Mach Port

1. 概念

Mach Port 是受内核保护的单向 IPC 通道、功能和名称。在 Mach 内核中,mach port 被实现成一个有限长度被内核所维护消息队列,与 Linux Pipe 有些相似,都会因为队列满或者队列空而阻塞,其基本操作为发送和接收消息。该队列是多生产者、单消费者队列,只能有单个 receive right。

Port 的这种抽象以及相关的操作是 mach 通信的基础。一个端口有着与之相关联的内核管理权限,而每个 task 都必须拥有 port 的适当权限才能操作它。当一个 Mach Message 被发送至某个 task 中,只有具有接收权限的 Mach port 才能接收该 Message,并将其从队列中删除。

例如这种权限设置可以允许一些任务向给定的端口发送信息,或者指定一些任务可以接收到发送给它的信息。

mach port 在 Mach 中非常重要,它表示着对象的引用,代表了OS中各类服务、资源等抽象。在 Mach 内核中,相当多的数据结构、服务等等都用 mach port 表示;而用户也可以通过对应的 mach port 来访问到 tasks、threads以及 memory objects。

Mach port 的名称是一个整数,但与文件描述符不同, Mach 端口不会通过 fork 而隐式继承。

2. Port Right

每个 Mach Port 都有着对应 port 的权限(right),以下是 Mac OSX 所定义的部分 port right 类型:

  • MACH_PORT_RIGHT_SEND:表示权限拥有者可以向该端口发送信息
  • MACH_PORT_RIGHT_RECEIVE:表示权限拥有者可以从该端口中获取 Message
  • MACH_PORT_RIGHT_SEND_ONCE:表示发送方只能发送一次 Message。不管该权限是否被销毁,该句柄始终会发送一条消息。
  • MACH_PORT_RIGHT_PORT_SET:表示多个 port name 的集合,可以被看做是多个端口接收权限的集合。端口集可用于同时侦听多个端口,类似于 Unix epoll 机制等等。
  • MACH_PORT_RIGHT_DEAD_NAME:只是一个占位符。若某个端口的权限被销毁后,则该端口的所有现有句柄的权限都将转换成 dead name(即无效权限)。dead name 机制是为了防止所接管的端口名被过早重用。

若某个端口的接收权限被释放时,则将该端口视为被销毁。注意接收句柄在任何时候都只能有一个 task 所持有。

端口权限名称(port right name)是某个 task 用来引用所持有的 port right 的特定整数值,有点类似文件描述符。需要注意的是每个port right name 只会在原始任务的上下文中有意义,这意味着即便将该名称发送给其他的任务,该任务也无法使用该名称访问对应的 mach port。(这也再次类似于文件描述符)

这个 port right name 正是我们日常见到最多的**用户层(注意必须指定是用户层)**中 mach_port_t 类型的值。

注意还有一个 port name(和 port right name 不一样),在用户层中是 mach_port_name_t 类型的值。

port name 和 right 的关系,类似于 Unix 中文件描述符和文件描述符权限的关系。但是,请勿直接将 right 等同于 权限,mach port right 和权限二字仍然有着较大的差别。

四、Mach Message

Mach IPC message 是线程之间相互通信的数据对象,它也是 tasks 之间通信的典型方式。一个 Message 中可能包含实际的数据(即内联数据),或者包含指向外联数据(out-of-line,OOL)的指针;后者是针对大数据传输的一种优化。

Mach Message 由以下几个部分组成:

  • 一个强制要有的消息头 (mach_msg_header_t 类型)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef   struct 
    {
    mach_msg_bits_t msgh_bits; // 一些消息标志位
    mach_msg_size_t msgh_size; // 消息 header + body + data 的总大小
    mach_port_t msgh_remote_port; // 目标 port right
    mach_port_t msgh_local_port; // 辅助 port right
    mach_port_name_t msgh_voucher_port;
    mach_msg_id_t msgh_id; // 传递 mach msg 时不会使用该字段,用户可自行设置该字段
    } mach_msg_header_t;
  • 一个可选的消息 body (mach_msg_body_t 类型)

    1
    2
    3
    4
    typedef struct
    {
    mach_msg_size_t msgh_descriptor_count;
    } mach_msg_body_t;

    注意,消息 body 并不只是这一个简简单单的结构体,请看下面的图。

  • 用户待发送的数据 data

  • 一个可选的 tailer(mach_msg_trailer_t 类型)。该字段只与发送方有关。这个我们将在下面讲到。

一个简单 Message 示例。其中 header.size 描述的是 header + data 的总大小:

Mach消息发送机制_第1张图片

一个复杂 Message 示例。与简单消息不同的是,复杂消息还包含了 body 信息,用以额外说明一些信息。

Mach消息发送机制_第2张图片

这个是更详细的说明图:

image-20211230113611950

这是一个复杂 Message 的具体代码样例。其中 body 部分包括 msgBody 字段和 ports[1] 字段,待发送 data 部分为 notifyHeader 字段:

1
2
3
4
5
6
struct PingMsg {
mach_msg_header_t msgHdr;
mach_msg_body_t msgBody;
mach_msg_port_descriptor_t ports[1];
OSNotificationHeader64 notifyHeader __attribute__ ((packed));
};

Message 的具体使用与机理将在下面使用中慢慢说明。

五、Mach API 入门使用

1. 单向 Mach 通信示例

*. 代码示例

以下是使用 Mach 低级 API 进行 IPC 的一个简单例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>

void sender() {
// 从 bootstrap 中查询并获取一个 mach port
mach_port_t port;
kern_return_t kr = bootstrap_look_up(bootstrap_port, "io.github.kiprey", &port);
assert(kr == KERN_SUCCESS);
printf("bootstrap_look_up() returned port right name %d\n", port);

// 构造待发送的信息
struct {
mach_msg_header_t header;
char texts[20];
int integer;
} message;

message.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
message.header.msgh_remote_port = port;
message.header.msgh_local_port = MACH_PORT_NULL;
message.header.msgh_size = sizeof(message);

strcpy(message.texts, "kiprey_texts");
message.integer = 123;

// 将其发送
mach_msg_return_t mr = mach_msg_send(&message.header);
assert(mr == KERN_SUCCESS);
printf("message is sent.\n");
}

void receiver() {
// 创建一个带有接收权限的 mach port
mach_port_t port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
assert(kr == KERN_SUCCESS);
printf("mach_port_allocate() created port right name %d\n", port);

// 给该 port 再增加一个发送权限
kr = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
assert (kr == KERN_SUCCESS);
printf("mach_port_insert_right() inserted a send right\n");

// 将该端口的 send right 发送给 bootstrap,这样就可以被其他进程所查询
kr = bootstrap_register(bootstrap_port, "io.github.kiprey", port);
assert (kr == KERN_SUCCESS);
printf("bootstrap_register()'ed our port\n");

// 等待 message
struct {
mach_msg_header_t header;
char texts[20];
int integer;
mach_msg_trailer_t trailer;
} message;

message.header.msgh_size = sizeof(message);
message.header.msgh_local_port = port;
kr = mach_msg_receive(&message.header);
assert (kr == KERN_SUCCESS);
printf("Got a message\n");

printf("Text: %s, number: %d\n", message.texts, message.integer);
}

int main(int argc, const char * argv[]) {
if(fork() == 0) {
// 等待 receiver 注册好 port 后再发送信息
sleep(1);
sender();
}
else {
receiver();
}
return 0;
}

测试结果:

image-20211225192910087

接下来将简单讲讲该例子中所调用的一些用户 API。

a. mach_port_allocate

初始时,接收端调用 mach_port_allocate 创建一个指定权限的 mach port:

1
2
mach_port_t port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);

该函数的定义如下:

1
kern_return_t mach_port_allocate(ipc_space_t task,  mach_port_right_t right, mach_port_name_t *name)

其中,第一个参数指定当前进程所在的 task。有趣的是,这种指定 task 的方式也是通过传递一个 mach port name 来完成。以下是 task_self_trap 函数的源代码,mach_task_self 函数是该函数的 wrapper。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* Routine: task_self_trap [mach trap]
* Purpose:
* Give the caller send rights for his own task port.
* Conditions:
* Nothing locked.
* Returns:
* MACH_PORT_NULL if there are any resource failures
* or other errors.
*/

mach_port_name_t
task_self_trap(
__unused struct task_self_trap_args *args)
{
task_t task = current_task();
ipc_port_t sright;
mach_port_name_t name;

sright = retrieve_task_self_fast(task);
name = ipc_port_copyout_send(sright, task->itk_space);
return name;
}

第二个参数指定当前待分配 Mach port 的 right,这里请求的是接收权限。根据 xnu 源码,该函数的第二个参数只有以下三种有效:

  • MACH_PORT_RIGHT_RECEIVE:创建一个新端口,且当前只有接收权限
  • MACH_PORT_RIGHT_PORT_SET:创建一个空的端口集,其中端口集里没有任何成员
  • MACH_PORT_RIGHT_DEAD_NAME :创建一个新的 dead name

该函数的第三个参数指定 成功分配 port 时其所存放的位置,这个没啥好说的,略过。

b. mach_port_insert_right

作用:将指定的 port right 插入进当前 task 中。

例子中的使用方式:

1
2
// 给该 port 再增加一个发送权限
kr = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);

其函数声明如下:

1
2
3
4
5
6
kern_return_t
mach_port_insert_right(
ipc_space_t task,
mach_port_name_t name,
mach_port_t poly,
mach_msg_type_name_t polyPoly)

在这个例子中,调用者会对新创建的 port (此时只有 receive right) 添加上 send right。这里的 send right 指的是给当前 port 发送 mach message 的权限

c. bootstrap_register/lookup

在 OSX 中,当一个新的 task 被创建时,它会被额外设置一组特殊的Mach port。其中包括:

  • 主机端口(host port,itk_host),表示运行该任务的机器。该端口允许 task 获取有关内核和主机的信息。
  • 任务端口(task port,itk_sself),即这个端口引用的任务是自己。这个端口不允许用于控制自生,貌似该端口只能用于获取 task info。
  • 引导端口(bootstrap port,itk_bootstrap),连接到 bootstrap server(launchd)。

剩余的可以在 osfmk\mach\task_special_ports.h 中了解。

对应与 task 内核结构体中的字段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* IPC structures */
struct ipc_port *itk_self; /* not a right, doesn't hold ref */
struct ipc_port *itk_nself; /* not a right, doesn't hold ref */
struct ipc_port *itk_sself; /* a send right */
struct exception_action exc_actions[EXC_TYPES_COUNT];
/* a send right each valid element */
// host port
struct ipc_port *itk_host; /* a send right */
// bootstrap port
struct ipc_port *itk_bootstrap; /* a send right */
// seatbelt port
struct ipc_port *itk_seatbelt; /* a send right */
// seatbelt port
struct ipc_port *itk_gssd; /* yet another send right */
// debug port
struct ipc_port *itk_debug_control; /* send right for debugmode communications */
// task_access port
struct ipc_port *itk_task_access; /* and another send right */
// resume port
struct ipc_port *itk_resume; /* a receive right to resume this task */
// 注册端口, 可以调用 mach_ports_register 进行注册
struct ipc_port *itk_registered[TASK_PORT_REGISTER_MAX];
/* all send rights */

struct ipc_space *itk_space;

可以发现这些 struct ipc_port itk_* 都是特殊的 mach port,每个 task 都会被设置。

其中,itk_hostitk_bootstrapitk_seatbeltitk_gssditk_task_access 都是从 parent task 中继承。

对于 itk_registered 数组来说,用户可以使用 mach_ports_register 函数将目标端口注册进该数组中,并使用 mach_ports_lookup 进行查询。注册后的 port right 将会填充至 task 结构体中 itk_registered 数组的某个槽。

bootstrap server 提供一个 port namespace,task 可以在其中注册自己的端口,其他 task 可以查找并向其发送消息。

我们可以将 bootstrap server 看作一个电话簿:task 可以放置一个已知且被命名的值,以对应于该 task 正在监听的 Mach port。

若某个 task 需要向 bootstrap server 注册服务,则 task 可以使用 bootstrap_register() 函数,该函数接受字符串名称和与之关联的Mach端口。但需要主要的是,Mac OSX 在10.5中弃用了这个函数,因此在编译上面的例子时,编译器会报出一个 Deprecated 的 warnning。

不过,我们还可以使用 bootstrap_check_in 来取代 bootstrap_register 函数。

在这个例子中,接收方会将带有 send right 的 mach port 注册进 bootstrap 中;那么当发送方尝试向 bootstrap 申请获取接收方的 port 时,bootstrap 就可以将当前所注册的 mach port 的 send right 复制一份给发送方

这样,发送方便有了该 mach port 的 send right,可以向该 port 发送数据。而 mach port 的另一端(也就是接收方)便可以直接读取到发送方发来的消息。

d. mach_msg

作用:发送 mach message 或者接收 mach message。在这个例子中,发送方和接收方都会间接调用到这个函数来发送或者接收 mach msg。

我们先简单看看 mach_msg 函数的定义,了解该函数各个参数的作用或功能,内核的具体处理方式将在后面讲到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
mach_msg_return_t
mach_msg(msg, option, send_size, rcv_size, rcv_name, timeout, notify)
mach_msg_header_t *msg; // 指向 Mach message 的指针
mach_msg_option_t option; // 一些基础标志,例如 MACH_SEND_MSG 或 MACH_RCV_MSG 标志以指定消息是发送还是接收
mach_msg_size_t send_size; // 待发送的消息长度
mach_msg_size_t rcv_size; // 待接收的消息长度
mach_port_t rcv_name; // 接收消息的 port
mach_msg_timeout_t timeout;// 指定 mach_msg 最长等待时间
mach_port_t notify; // 一个通知 port,用于接收通知信息

mach_msg_return_t
mach_msg_send(mach_msg_header_t *msg)
{
return mach_msg(msg, MACH_SEND_MSG,
msg->msgh_size, 0, MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
}

mach_msg_return_t
mach_msg_receive(mach_msg_header_t *msg)
{
return mach_msg(msg, MACH_RCV_MSG,
0, msg->msgh_size, msg->msgh_local_port,
MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
}

对于发送方而言,发送方需要指定 header 的一些字段:

1
2
3
4
message.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0); // 设置下面对应 port 的 mach 信息类型
message.header.msgh_remote_port = port; // 设置发送端口为目标 port
message.header.msgh_local_port = MACH_PORT_NULL; // 没有辅助端口
message.header.msgh_size = sizeof(message);

2. 双向 mach 通信示例

上面的例子已经为我们展示了单向 mach 通信的基本方式(sender-> receiver)。接下来尝试让receiver也能发送数据给sender,实现双向通信。

需要注意的是, mach 是单向通信,因此必须让 sender 再创建一个新的 port(即 sender 持有新 mach port,注意此时 receiver 已经持有了一个旧的 mach port),并让 receiver 持有该 port 的 send right 才能实现双向通信。而这就涉及到一个问题:如何传递 mach port right?

一种解法是,再次利用 bootstrap 做中转,这确实是一个解决方法,但是不够优雅。实际上,因为此时的 sender 是可以通过已有的 mach port 将信息发送给 receiver,因此我们可以利用这个 mach port ,将新的 mach port 的 send right 发送给 receiver

因为 Mach message 是支持传输 port right 的。

以下是整个通信的完整过程,其中 bob 是 sender, alice 是 receiver:

img

现在的问题是,如何把权限发送过去?我们分别看看两种不同的方式。

a. reply port

1) sender

当 sender 从 bootstrap 中获取到了 receiver mach port 的 send right 后,sender 便可以给 receiver 发送信息。这是之前的 message header 设置方式:

1
2
3
message.header.msgh_bits = MACH_MSGH_BITS(/* remote */ MACH_MSG_TYPE_COPY_SEND, /* local */0);
message.header.msgh_remote_port = port;
message.header.msgh_local_port = MACH_PORT_NULL;

但在这里,我们将使用一个新的 message 方式:

  1. 在 msgh_bits 中额外设置 local port 的 right 为 MACH_MSG_TYPE_MAKE_SEND_ONCE,这会使得对端只能向该端口发送一次信息
  2. 在 msgh_local_port 字段中放入本地自己新建立的 replyPort 端口。
1
2
3
4
5
6
7
8
9
10
11
12
message.header.msgh_bits = MACH_MSGH_BITS_SET(
/* remote */ MACH_MSG_TYPE_COPY_SEND,
/* local */ MACH_MSG_TYPE_MAKE_SEND_ONCE,
/* voucher */ 0,
/* other */ 0);
// 注: 上面这条语句等价于
// message.header.msgh_bits = MACH_MSGH_BITS(/* remote */ MACH_MSG_TYPE_COPY_SEND, /* local */ MACH_MSG_TYPE_MAKE_SEND_ONCE);

message.header.msgh_remote_port = port;

// 与之前单向通信设置 MACH_PORT_NULL 不同,这里设置了一个 sender 自己创建并带有 send right 的 mach port
message.header.msgh_local_port = replyPort;

那么此时再使用 mach_msg 发送这条 message,则 sender 发送来的信息中将包含一个 replyPort。

这个 replyPort 有什么用呢?事实上,对面的 receiver 将会通过这个传过去的 replyPort,向这边的 sender 发送信息。

注意所设置的 message.header.msgh_bits,其中 local 部分对应的是 MACH_MSG_TYPE_MAKE_SEND_ONCE, 这意味着 replyPort 只能被 receiver 使用一次 send 操作。

2) receiver

当 receiver 接收 message 时,sender 发送信息时的 remote_portlocal_port,分别一一对应于 receiver 所接收到 message 中的 local_portremote_port

因此此时 receiver 方的 message 中 remote_port 不会是 MACH_PORT_NULL,而是先前设置的 replyPort

因此接下来 receiver 便可以通过这个 replyPort 向 sender 发送信息。但需要注意的是,在发送信息给 replyPort 时,其 message.header.msgh_bits 字段,必须设置成 MACH_MSG_TYPE_MAKE_SEND_ONCE,即和发送该端口过来时所设置的位一致。

因为受到发送 replyPort 方(即 sender 方)的设置或者限制, receivier 方只能发送一次信息至 replyPort 中。

3) 代码示例

以下是完整的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>

struct mach_msg_send_t {
mach_msg_header_t header;
char texts[0x20];
int integer;
};

struct mach_msg_receive_t {
struct mach_msg_send_t recv_content;
mach_msg_trailer_t trailer;
};

void sender()
{
// 从 bootstrap 中查询并获取一个 mach port
mach_port_t port;
kern_return_t kr = bootstrap_look_up(bootstrap_port, "io.github.kiprey", &port);
assert(kr == KERN_SUCCESS);
printf("[sender] bootstrap_look_up() returned port right name %d\n", port);

// 构造待发送的信息
struct mach_msg_send_t send_msg;
strcpy(send_msg.texts, "Hello, I'm sender.");
send_msg.integer = 1;

// 新建立一个 receiver 发送的 replyPort
mach_port_t replyPort;
kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &replyPort);
assert(kr == KERN_SUCCESS);
printf("[sender] mach_port_allocate() created port right name %d\n", replyPort);

// 给该 port 再增加一个发送权限
kr = mach_port_insert_right(mach_task_self(), replyPort, replyPort, MACH_MSG_TYPE_MAKE_SEND);
assert(kr == KERN_SUCCESS);
printf("[sender] mach_port_insert_right() inserted a send right\n");

// 注意这里,remote port 的发送权限是 MACH_MSG_TYPE_MAKE_SEND_ONCE
send_msg.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND_ONCE);
send_msg.header.msgh_remote_port = port;
send_msg.header.msgh_local_port = replyPort;
send_msg.header.msgh_size = sizeof(send_msg);

// 将其发送
mach_msg_return_t mr = mach_msg_send(&send_msg.header);
assert(mr == KERN_SUCCESS);
printf("[sender] Message is sent.\n");

// 等待 message
struct mach_msg_receive_t recv_msg;
recv_msg.recv_content.header.msgh_size = sizeof(recv_msg);
recv_msg.recv_content.header.msgh_local_port = replyPort;
kr = mach_msg_receive(&recv_msg.recv_content.header);
assert(kr == KERN_SUCCESS);
printf("[sender] Got a Message\n");
printf("[sender] Text: %s | number: %d\n", recv_msg.recv_content.texts, recv_msg.recv_content.integer);
}

void receiver()
{
// 创建一个带有接收权限的 mach port
mach_port_t port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
assert(kr == KERN_SUCCESS);
printf("[receiver] mach_port_allocate() created port right name %d\n", port);

// 给该 port 再增加一个发送权限
kr = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
assert(kr == KERN_SUCCESS);
printf("[receiver] mach_port_insert_right() inserted a send right\n");

// 将该端口的 send right 发送给 bootstrap,这样就可以被其他进程所查询
kr = bootstrap_register(bootstrap_port, "io.github.kiprey", port);
assert(kr == KERN_SUCCESS);
printf("[receiver] bootstrap_register()'ed our port\n");

// 等待 message
struct mach_msg_receive_t recv_msg;
recv_msg.recv_content.header.msgh_size = sizeof(recv_msg);
recv_msg.recv_content.header.msgh_local_port = port;
kr = mach_msg_receive(&recv_msg.recv_content.header);
assert(kr == KERN_SUCCESS);
printf("[receiver] Got a Message\n");
printf("[receiver] Text: %s | number: %d | remote_port: %d\n", recv_msg.recv_content.texts, recv_msg.recv_content.integer, recv_msg.recv_content.header.msgh_remote_port);

struct mach_msg_send_t send_msg;
strcpy(send_msg.texts, "Hello, I'm receiver.");
send_msg.integer = 2;

// 注意这里的发送权限是 MACH_MSG_TYPE_MAKE_SEND_ONCE
send_msg.header.msgh_bits = recv_msg.recv_content.header.msgh_bits & MACH_MSGH_BITS_REMOTE_MASK;
send_msg.header.msgh_remote_port = recv_msg.recv_content.header.msgh_remote_port;
send_msg.header.msgh_local_port = MACH_PORT_NULL;
send_msg.header.msgh_size = sizeof(send_msg);

// 将其发送
mach_msg_return_t mr = mach_msg_send(&send_msg.header);
assert(mr == KERN_SUCCESS);
printf("[receiver] Message is sent.\n");
}

int main(int argc, const char *argv[])
{
if (fork() == 0)
sender();
else
receiver();
return 0;
}

执行效果:

image-20211229213351546

那么可能会有疑问,为什么 replyPort 的 msg 类型要设置成 MACH_MSG_TYPE_MAKE_SEND_ONCE?能不能设置成 MACH_MSG_TYPE_COPY_SEND ?实际上是可以的,并且后者可以允许 receiver 多次向 replyPort 发送 mach message,而不是只有一次

b. complex message

还记得之前描述 Mach Message 的结构么?Mach message 既可以传递简单信息(即之前的那些示例)又可以传递复杂信息(即接下来要讲的)。现在,我们将尝试使用复杂 mach message 来传递一个通信 mach port。

为了简化说明,这里假设上面的内容已经完全理解。

1) sender

现在, sender 需要尝试将自己新建好的 replyPort(已完成包括 alloc, insert right 等操作) 发给 receiver,那该怎么做呢?

其实可以直接在消息主体中,传递端口描述符。这里需要先引入一下待发送的 mach msg 结构类型定义:

1
2
3
4
5
typedef struct {
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_port_descriptor_t descriptor;
} mach_msg_complex_send_t;

其中,header 自不必说;msgh_descriptor_count 说明接下来将会有多少个 descriptor;而mach_msg_port_descriptor_t 类型的 descriptor 字段将会描述一些关于待传递 port 的信息。

每个 descriptor 不管是什么类型,都会占用 40 字节。以下是最原始的 descriptor 的类型声明:

1
2
3
4
5
6
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;

而端口描述符的定义如下:

1
2
3
4
5
6
7
typedef struct{
mach_port_t name;
mach_msg_size_t pad1;
unsigned int pad2 : 16;
mach_msg_type_name_t disposition : 8;
mach_msg_descriptor_type_t type : 8;
} mach_msg_port_descriptor_t;

其中

  • name:待传递的 port。这里要设置为 replyPort

  • disposition:待传递 port 的 right。这里设置为 MACH_MSG_TYPE_PORT_SEND

    一共有以下几种:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /*
    * Values received/carried in messages. Tells the receiver what
    * sort of port right he now has.
    *
    * MACH_MSG_TYPE_PORT_NAME is used to transfer a port name
    * which should remain uninterpreted by the kernel. (Port rights
    * are not transferred, just the port name.)
    */

    #define MACH_MSG_TYPE_PORT_NONE 0

    #define MACH_MSG_TYPE_PORT_NAME 15
    #define MACH_MSG_TYPE_PORT_RECEIVE MACH_MSG_TYPE_MOVE_RECEIVE
    #define MACH_MSG_TYPE_PORT_SEND MACH_MSG_TYPE_MOVE_SEND
    #define MACH_MSG_TYPE_PORT_SEND_ONCE MACH_MSG_TYPE_MOVE_SEND_ONCE
  • type:待传递的类型。这里要设置为 MACH_MSG_PORT_DESCRIPTOR

    由于 descriptor 的类型不只是端口描述符一种,因此需要显式为 descriptor 指定类型,以便于内核处理。共有以下几种类型:

    1
    2
    3
    4
    5
    #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
    #define MACH_MSG_GUARDED_PORT_DESCRIPTOR 4

代码示例:

1
2
3
4
send_msg.msgh_descriptor_count      = 1;
send_msg.descriptor.name = replyPort;
send_msg.descriptor.disposition = MACH_MSG_TYPE_PORT_SEND;
send_msg.descriptor.type = MACH_MSG_PORT_DESCRIPTOR;

最后执行 mach_msg_send 之前,别忘记向 msgh_bits 字段中添加 MACH_MSGH_BITS_COMPLEX,以指定该信息为复杂信息。否则这些描述符只会被解释成内联信息

1
2
// 注意这里,要指定待发送的信息格式为 complex
send_msg.header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
2) receiver

接收端只需接收发送端发来的数据,并取出端口描述符中的 port name,即可开始通信。

要做的事情较为简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 等待 message
mach_msg_complex_receive_t recv_msg;
recv_msg.recv_content.header.msgh_size = sizeof(recv_msg);
recv_msg.recv_content.header.msgh_local_port = port;
kr = mach_msg_receive(&recv_msg.recv_content.header);

mach_msg_simple_send_t send_msg;
strcpy(send_msg.texts, "Hello, I'm receiver.");
send_msg.integer = 2;

send_msg.header.msgh_bits = recv_msg.recv_content.descriptor.disposition;
send_msg.header.msgh_remote_port = recv_msg.recv_content.descriptor.name;
send_msg.header.msgh_local_port = MACH_PORT_NULL;
send_msg.header.msgh_size = sizeof(send_msg);

// 将其发送
mach_msg_return_t mr = mach_msg_send(&send_msg.header);
3) 代码示例

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>

typedef struct {
mach_msg_header_t header;
char texts[0x20];
int integer;
} mach_msg_simple_send_t;

typedef struct {
mach_msg_simple_send_t recv_content;
mach_msg_trailer_t trailer;
} mach_msg_simple_receive_t;

typedef struct {
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_port_descriptor_t descriptor;
} mach_msg_complex_send_t;

typedef struct {
mach_msg_complex_send_t recv_content;
mach_msg_trailer_t trailer;
} mach_msg_complex_receive_t;

void sender()
{
// 等待一小会,让 receiver 注册一下 bootstrap
usleep(100);
// 从 bootstrap 中查询并获取一个 mach port
mach_port_t port;
kern_return_t kr = bootstrap_look_up(bootstrap_port, "io.github.kiprey", &port);
assert(kr == KERN_SUCCESS);
printf("[sender] bootstrap_look_up() returned port right name %d\n", port);

// 构造待发送的信息
mach_msg_complex_send_t send_msg;

// 新建立一个 receiver 发送的 replyPort
mach_port_t replyPort;
kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &replyPort);
assert(kr == KERN_SUCCESS);
printf("[sender] mach_port_allocate() created port right name %d\n", replyPort);

// 给该 port 再增加一个发送权限
kr = mach_port_insert_right(mach_task_self(), replyPort, replyPort, MACH_MSG_TYPE_MAKE_SEND);
assert(kr == KERN_SUCCESS);
printf("[sender] mach_port_insert_right() inserted a send right\n");

// 注意这里,要指定待发送的信息格式为 complex
send_msg.header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
send_msg.header.msgh_remote_port = port;
send_msg.header.msgh_local_port = MACH_PORT_NULL;
send_msg.header.msgh_size = sizeof(send_msg);
// 指定只有一个描述符需要传递
send_msg.msgh_descriptor_count = 1;
send_msg.descriptor.name = replyPort;
send_msg.descriptor.disposition = MACH_MSG_TYPE_PORT_SEND;
send_msg.descriptor.type = MACH_MSG_PORT_DESCRIPTOR;

// 将其发送
mach_msg_return_t mr = mach_msg_send(&send_msg.header);
assert(mr == KERN_SUCCESS);
printf("[sender] Message is sent.\n");

// 等待 message
mach_msg_simple_receive_t recv_msg;
recv_msg.recv_content.header.msgh_size = sizeof(recv_msg);
recv_msg.recv_content.header.msgh_local_port = replyPort;
kr = mach_msg_receive(&recv_msg.recv_content.header);
assert(kr == KERN_SUCCESS);
printf("[sender] Got a Message\n");
printf("[sender] Text: %s | number: %d\n", recv_msg.recv_content.texts, recv_msg.recv_content.integer);
}

void receiver()
{
// 创建一个带有接收权限的 mach port
mach_port_t port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
assert(kr == KERN_SUCCESS);
printf("[receiver] mach_port_allocate() created port right name %d\n", port);

// 给该 port 再增加一个发送权限
kr = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
assert(kr == KERN_SUCCESS);
printf("[receiver] mach_port_insert_right() inserted a send right\n");

// 将该端口的 send right 发送给 bootstrap,这样就可以被其他进程所查询
kr = bootstrap_register(bootstrap_port, "io.github.kiprey", port);
assert(kr == KERN_SUCCESS);
printf("[receiver] bootstrap_register()'ed our port\n");

// 等待 message
mach_msg_complex_receive_t recv_msg;
recv_msg.recv_content.header.msgh_size = sizeof(recv_msg);
recv_msg.recv_content.header.msgh_local_port = port;
kr = mach_msg_receive(&recv_msg.recv_content.header);
assert(kr == KERN_SUCCESS);
assert(recv_msg.recv_content.msgh_descriptor_count == 1);
printf("[receiver] Got a Message\n");
printf("[receiver] remote_port: %d\n", recv_msg.recv_content.descriptor.name);

mach_msg_simple_send_t send_msg;
strcpy(send_msg.texts, "Hello, I'm receiver.");
send_msg.integer = 2;

// 注意这里的发送权限是 MACH_MSG_TYPE_MAKE_SEND_ONCE
send_msg.header.msgh_bits = recv_msg.recv_content.descriptor.disposition;
send_msg.header.msgh_remote_port = recv_msg.recv_content.descriptor.name;
send_msg.header.msgh_local_port = MACH_PORT_NULL;
send_msg.header.msgh_size = sizeof(send_msg);

// 将其发送
mach_msg_return_t mr = mach_msg_send(&send_msg.header);
assert(mr == KERN_SUCCESS);
printf("[receiver] Message is sent.\n");
}

int main(int argc, const char *argv[])
{
fork() ? sender() : receiver();
return 0;
}

运行结果如下:

image-20211229233517941

3. mach OOL 通信

当某个进程需要传递大量数据给对端时,simple message 中的内联数据已经无法满足我们的需求了(因为将数据拷贝进内联数据的开销是相当大的)。因此,我们可以试着使用 mach complex message 中的 OOL 描述符传递内存页

a. sender

首先,我们需要定义一下复杂 mach msg 的结构:

1
2
3
4
5
6
typedef struct
{
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_ool_descriptor_t descriptor;
} mach_msg_complex_send_t;

注意到消息体中的描述符为 mach_msg_ool_descriptor_t 类型。该类型的结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct{
void* address;
#if !defined(__LP64__)
mach_msg_size_t size;
#endif
boolean_t deallocate: 8;
mach_msg_copy_options_t copy: 8;
unsigned int pad1: 8;
mach_msg_descriptor_type_t type: 8;
#if defined(__LP64__)
mach_msg_size_t size;
#endif
} mach_msg_ool_descriptor_t;

其中,

  • address 字段:存放待发送内存页面的基地址。

  • size 字段:待发送内存长度。

  • deallocate 字段:发送内存页面后,指定发送者是否需要隐式释放已发送的内存页面(例如自动调用 vm_deallocate),通常是 false。

    这个字段可以将 内存复制 转换成 内存移动,即将发送方的内存页移动到接收方的进程中,内存处理效率更高。

  • copy 字段:指定内核以什么方式来复制发送过来的内存页面。共有两种方式:

    • MACH_MSG_VIRTUAL_COPY:允许内核选择任何机制来传输数据。通常内核会先复制虚拟页面,共享物理页面,直到实际写入操作的发生再来进行数据复制操作,即写时复制。
    • MACH_MSG_PHYSICAL_COPY:内核会实际复制数据至新的物理页中。
  • type 字段:指定当前 descriptor 的类型,这里必须为 MACH_MSG_OOL_DESCRIPTOR

接下来,sender 需要创建一个虚拟页面,并在该页面上写入一些数据:

1
2
3
4
5
6
char *buf = NULL;
vm_size_t len = vm_page_size;
if (vm_allocate(mach_task_self(), (vm_address_t *)&buf, len,
VM_PROT_READ | VM_PROT_WRITE) != KERN_SUCCESS)
abort();
strcpy(buf, "This is a buf message from sender.");

然后设置 Message,并将其发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 注意这里,要指定待发送的信息格式为 complex
send_msg.header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
send_msg.header.msgh_remote_port = port;
send_msg.header.msgh_local_port = MACH_PORT_NULL;
send_msg.header.msgh_size = sizeof(send_msg);

// 设置 OOL 描述符信息
send_msg.msgh_descriptor_count = 1;
send_msg.descriptor.address = buf;
send_msg.descriptor.copy = MACH_MSG_VIRTUAL_COPY;
send_msg.descriptor.deallocate = false;
send_msg.descriptor.size = len;
send_msg.descriptor.type = MACH_MSG_OOL_DESCRIPTOR;

b. receiver

当接收方接收这个 mach message 时,在接收方的地址空间中,内核将新分配一块内存用于存放接收到的数据。

原先有一个选项用于指定内核将接收到的数据覆盖至接收方指定的内存地址处(MACH_MSG_OVERWRITE),但这个选项已经被废弃。

c. 代码示例

以下是一个简单的代码示例,其中接收方使用 MACH_MSG_ALLOCATE 方式来接收数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>

typedef struct
{
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_ool_descriptor_t descriptor;
} mach_msg_complex_send_t;

typedef struct
{
mach_msg_complex_send_t recv_content;
mach_msg_trailer_t trailer;
} mach_msg_complex_receive_t;

void sender()
{
// 等待一小会,让 receiver 注册一下 bootstrap
usleep(1000);
// 从 bootstrap 中查询并获取一个 mach port
mach_port_t port;
if (bootstrap_look_up(bootstrap_port, "io.github.kiprey", &port) != KERN_SUCCESS)
abort();
printf("[sender] bootstrap_look_up() returned port right name %d\n", port);

// 构造待发送的信息
mach_msg_complex_send_t send_msg;

// 注意这里,要指定待发送的信息格式为 complex
send_msg.header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
send_msg.header.msgh_remote_port = port;
send_msg.header.msgh_local_port = MACH_PORT_NULL;
send_msg.header.msgh_size = sizeof(send_msg);
// 指定待传递的地址
char *buf = NULL;
vm_size_t len = vm_page_size;
if (vm_allocate(mach_task_self(), (vm_address_t *)&buf, len,
VM_PROT_READ | VM_PROT_WRITE) != KERN_SUCCESS)
abort();
strcpy(buf, "This is a buf message from sender.");

send_msg.msgh_descriptor_count = 1;
send_msg.descriptor.address = buf;
send_msg.descriptor.copy = MACH_MSG_VIRTUAL_COPY;
send_msg.descriptor.deallocate = false;
send_msg.descriptor.size = len;
send_msg.descriptor.type = MACH_MSG_OOL_DESCRIPTOR;

// 将其发送
if (mach_msg_send(&send_msg.header) != KERN_SUCCESS)
abort();
printf("[sender] Message is sent, buf address: %#p\n", buf);
}

void receiver()
{
// 创建一个带有接收权限的 mach port
mach_port_t port;
if (mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port) != KERN_SUCCESS)
abort();
printf("[receiver] mach_port_allocate() created port right name %d\n", port);

// 给该 port 再增加一个发送权限
if (mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND) != KERN_SUCCESS)
abort();
printf("[receiver] mach_port_insert_right() inserted a send right\n");

// 将该端口的 send right 发送给 bootstrap,这样就可以被其他进程所查询
if (bootstrap_register(bootstrap_port, "io.github.kiprey", port) != KERN_SUCCESS)
abort();
printf("[receiver] bootstrap_register()'ed our port\n");

// 等待 message
mach_msg_complex_receive_t recv_msg;
recv_msg.recv_content.header.msgh_size = sizeof(recv_msg);
recv_msg.recv_content.header.msgh_local_port = port;
if (mach_msg_receive(&recv_msg.recv_content.header) != KERN_SUCCESS)
abort();
assert(recv_msg.recv_content.msgh_descriptor_count == 1);

char *buf = recv_msg.recv_content.descriptor.address;
size_t len = recv_msg.recv_content.descriptor.size;
printf("[receiver] Got a Message\n");
printf("[receiver] recv buf address: %#p, len: %d, content: %s\n", buf, len, buf);
}

int main(int argc, const char *argv[])
{
fork() ? sender() : receiver();
return 0;
}

测试结果:

image-20211230091913991

4. Message Trailer

接收方接收到的 Mach message 会包含一个 trailer 结构体。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct
{
mach_msg_header_t header;
char texts[0x20];
int integer;
} mach_msg_send_t;

typedef struct
{
mach_msg_send_t recv_content;
mach_msg_trailer_t trailer;
} mach_msg_receive_t;

其中,mach_msg_trailer_t结构体中有如下几种字段:

1
2
3
4
typedef struct{
mach_msg_trailer_type_t msgh_trailer_type;
mach_msg_trailer_size_t msgh_trailer_size;
} mach_msg_trailer_t;

第一个字段表示 trailer 的类型,第二个字段表示接下来 trailer 的个数。

对于 trailer 类型来说,目前 Mac OSX 对用户层来说只提供了一种格式,即MACH_MSG_TRAILER_FORMAT_0

1
2
3
typedef unsigned int mach_msg_trailer_type_t;

#define MACH_MSG_TRAILER_FORMAT_0 0

但是,该格式下有许多种 trailer 的类型,分别有:

  1. mach_msg_trailer_t:一个空的 trailer,只包含了 type 和 size 字段。

  2. mach_msg_seqno_trailer_t:在第1个结构体的内存布局基础之上,额外增添第3个字段

    1
    2
    3
    typedef natural_t mach_port_seqno_t;            /* sequence number */

    mach_port_seqno_t msgh_seqno;

    sequence number,即消息序列号

  3. mach_msg_security_trailer_t:在第2个结构体之上,额外增添第4个字段:

    1
    2
    3
    4
    5
    typedef struct{
    unsigned int val[2];
    } security_token_t;

    security_token_t msgh_sender;

    security token 的两个整数分别表示发送方的 UID 和 GID。

  4. mach_msg_audit_trailer_t:在第3个结构体之上,额外增添第5个字段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /*
    * The audit token is an opaque token which identifies
    * Mach tasks and senders of Mach messages as subjects
    * to the BSM audit system. Only the appropriate BSM
    * library routines should be used to interpret the
    * contents of the audit token as the representation
    * of the subject identity within the token may change
    * over time.
    */
    typedef struct{
    unsigned int val[8];
    } audit_token_t;

    audit_token_t msgh_audit;

    audit token 中共有 8 个整型,该 token 需要使用其他处理例程来进行解释。

  5. mach_msg_context_trailer_t:在第4个结构体之上,额外增添第6个字段

  6. mach_msg_mac_trailer_t:在第5个结构体之上,额外增添第7个字段

  7. mach_msg_max_trailer_t:在第6个结构体之上,额外增添第8个字段

可以看到,每一个 trailer 总是嵌套在下一个 trailer 之中,这有利于兼容。

接收者在接收 mach messag 时,必须显式指定 mach_msg 函数的 option 字段,以说明接收的 trailer 的类型为 FORMAT_0,同时指定接收 trailer 时终止接收的那个字段。请看下面这个例子:

1
2
3
4
5
6
7
8
// 等待 message
mach_msg_receive_t message;
mach_msg_option_t option = MACH_RCV_MSG
| MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)
| MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_SENDER);
kr = mach_msg(&message.recv_content.header, option,
0, sizeof(message), port,
MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

在这个例子中,option 设置了 MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_SENDER),这个操作是为了指定接收 mach_msg_security_trailer_t 类型的 trailer,因为该类型的最后一个字段为 sender

1
2
3
4
#define MACH_RCV_TRAILER_NULL   0 // mach_msg_trailer_t 
#define MACH_RCV_TRAILER_SEQNO 1 // mach_msg_trailer_seqno_t
#define MACH_RCV_TRAILER_SENDER 2 // mach_msg_security_trailer_t
#define MACH_RCV_TRAILER_AUDIT 3 // mach_msg_audit_trailer_t

以下是一个简单的测试例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>

typedef struct
{
mach_msg_header_t header;
char texts[0x20];
int integer;
} mach_msg_send_t;

typedef struct
{
mach_msg_send_t recv_content;
mach_msg_security_trailer_t trailer;
} mach_msg_receive_t;

void sender() {
printf("[sender] Current UID(%d) GID(%d)\n", getuid(), getgid());
usleep(1000);
// 从 bootstrap 中查询并获取一个 mach port
mach_port_t port;
kern_return_t kr = bootstrap_look_up(bootstrap_port, "io.github.kiprey", &port);
assert(kr == KERN_SUCCESS);
printf("[sender] bootstrap_look_up() returned port right name %d\n", port);

// 构造待发送的信息
mach_msg_send_t message;

message.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
message.header.msgh_remote_port = port;
message.header.msgh_local_port = MACH_PORT_NULL;
message.header.msgh_size = sizeof(message);

strcpy(message.texts, "Hello, I'm sender");
message.integer = 123;

// 将其发送
mach_msg_return_t mr = mach_msg_send(&message.header);
assert(mr == KERN_SUCCESS);
printf("[sender] Message is sent.\n");
}

void receiver() {
// 创建一个带有接收权限的 mach port
mach_port_t port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
assert(kr == KERN_SUCCESS);
printf("[receiver] mach_port_allocate() created port right name %d\n", port);

// 给该 port 再增加一个发送权限
kr = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
assert (kr == KERN_SUCCESS);
printf("[receiver] mach_port_insert_right() inserted a send right\n");

// 将该端口的 send right 发送给 bootstrap,这样就可以被其他进程所查询
kr = bootstrap_register(bootstrap_port, "io.github.kiprey", port);
assert (kr == KERN_SUCCESS);
printf("[receiver] bootstrap_register()'ed our port\n");

// 等待 message
mach_msg_receive_t message;
mach_msg_option_t option = MACH_RCV_MSG
| MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)
| MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_SENDER);
kr = mach_msg(&message.recv_content.header, option,
0, sizeof(message), port,
MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

assert (kr == KERN_SUCCESS);
printf("[receiver] Got a message\n");

printf("[receiver] Text: %s, number: %d\n", message.recv_content.texts, message.recv_content.integer);
printf("[receiver] Security token = UID(%u) GID(%u)\n",
message.trailer.msgh_sender.val[0], // sender's user ID
message.trailer.msgh_sender.val[1]); // sender's group ID
}

int main(int argc, const char * argv[]) {
fork() ? sender() : receiver();
return 0;
}

测试结果:

image-20211230155320740

六、部分内核类型介绍

1. ipc_space

对于 task 结构体中,其内部存在一个 struct ipc_space *itk_space 的字段,以存放当前 task 所使用的 IPC 信息,其结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ipc_space {
lck_spin_t is_lock_data;
ipc_space_refs_t is_bits; /* holds refs, active, growing */
ipc_entry_num_t is_table_size; /* current size of table */
ipc_entry_num_t is_table_free; /* count of free elements */
ipc_entry_t is_table; /* an array of entries */
task_t is_task; /* associated task */
struct ipc_table_size *is_table_next; /* info for larger table */
ipc_entry_num_t is_low_mod; /* lowest modified entry during growth */
ipc_entry_num_t is_high_mod; /* highest modified entry during growth */
struct bool_gen bool_gen; /* state for boolean RNG */
unsigned int is_entropy[IS_ENTROPY_CNT]; /* pool of entropy taken from RNG */
int is_node_id; /* HOST_LOCAL_NODE, or remote node if proxy space */
};

字段 is_table 指向一个元素类型为 struct ipc_entry 的数组,长度为 is_table_size,通常用户层使用的 mach port name (整型表示)将会映射到内核层的该结构体。is_table 在创建时就会存放一些初始条目。

字段 is_bits 包含了较多的控制信息,例如引用计数、当前 ipc_space 是否激活(active) 以及当前 ipc_space是否正在增大内存空间(growing)。其中 growing 位是为了防止条件竞争所设定的一个简单比特。内核使用 ipc_space 时,如果发现当前 ipc_space 的 is_table 大小不够,则会尝试进行 grow 操作;但如果当前内核线程发现当前 ipc_space 正在被其他内核线程 growing 时,则会先休眠(is_write_sleep),直到其他线程完成处理后再来进行接下来的操作。

当某个 mach port 的 receive right 被释放了,则这个 mach port 便视为被释放了,若此时持有该 mach port 的引用为 0 ,则 is_table 中对应的 ipc_entry 结构体将被移动至 is_table_free 中,并且被释放的 mach port 的所有 right 都被更改为 MACH_PORT_RIGHT_DEAD_NAME,表示这些 right 全都 dead。

这种机制是为了,防止所接管的 port name 被过早的重用。

若当前的 ipc_space 需要创建一个新的 ipc_entry 时,首先 ipc_space 会尝试从 is_table_free 中取出最早被释放的 ipc_entry(即 is_table_free 为 FIFO)并重用;但若 is_table_free 为空,则将尝试 扩大(grow) ipc_space,并插入一个新的 ipc_entry 结构体。

需要注意的是,即便某个 mach port 的 receive right 已经被释放了,那么如果该 mach port 的引用不为 0 (此时 mach port 的各个 right 为 Dead name),则在下次分配 mach port 时,仍然不能重用该 mach port name。

2. ipc_entry

用户层的 mach port name(整数表示)实际上对应至内核中 task->ipc_space->is_table 上的某个 ipc_entry 条目。而 ipc_entry 结构声明如下:

1
2
3
4
5
6
7
8
9
struct ipc_entry {
struct ipc_object *ie_object;
ipc_entry_bits_t ie_bits;
mach_port_index_t ie_index;
union {
mach_port_index_t next; /* next in freelist, or... */
ipc_table_index_t request; /* dead name request notify */
} index;
};

其中 ie_object 指针字段,实际指向的结构体有两种:ipc_portipc_pset

ie_bits 标志位字段保存了给定 port name 所代表的 right 类型。

3. ipc_port

ipc_port 结构体,对应于单个 mach port。该结构体记录了 Mach message 队列、mach port 的接收方和发送方 port、内核存储的相关数据等等。这些字段不一一解释,有用到再说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
struct ipc_port {

/*
* Initial sub-structure in common with ipc_pset
* First element is an ipc_object second is a
* message queue
*/
struct ipc_object ip_object;
struct ipc_mqueue ip_messages;

union {
struct ipc_space *receiver;
struct ipc_port *destination;
ipc_port_timestamp_t timestamp;
} data;

union {
ipc_kobject_t kobject;
ipc_importance_task_t imp_task;
ipc_port_t sync_qos_override_port;
} kdata;

struct ipc_port *ip_nsrequest;
struct ipc_port *ip_pdrequest;
struct ipc_port_request *ip_requests;
union {
struct ipc_kmsg *premsg;
struct {
sync_qos_count_t sync_qos[THREAD_QOS_LAST];
sync_qos_count_t special_port_qos;
} qos_counter;
} kdata2;

mach_vm_address_t ip_context;

natural_t ip_sprequests:1, /* send-possible requests outstanding */
ip_spimportant:1, /* ... at least one is importance donating */
ip_impdonation:1, /* port supports importance donation */
ip_tempowner:1, /* dont give donations to current receiver */
ip_guarded:1, /* port guarded (use context value as guard) */
ip_strict_guard:1, /* Strict guarding; Prevents user manipulation of context values directly */
ip_specialreply:1, /* port is a special reply port */
ip_link_sync_qos:1, /* link the special reply port to destination port */
ip_impcount:24; /* number of importance donations in nested queue */

mach_port_mscount_t ip_mscount;
mach_port_rights_t ip_srights;
mach_port_rights_t ip_sorights;

#if MACH_ASSERT
#define IP_NSPARES 4
#define IP_CALLSTACK_MAX 16
/* queue_chain_t ip_port_links;*//* all allocated ports */
thread_t ip_thread; /* who made me? thread context */
unsigned long ip_timetrack; /* give an idea of "when" created */
uintptr_t ip_callstack[IP_CALLSTACK_MAX]; /* stack trace */
unsigned long ip_spares[IP_NSPARES]; /* for debugging */
#endif /* MACH_ASSERT */
};

4. ipc_pset

ipc_pset 结构体,对应于多个 mach port 的集合。以下是其声明:

1
2
3
4
5
6
7
8
struct ipc_pset {

/*
* Initial sub-structure in common with all ipc_objects.
*/
struct ipc_object ips_object;
struct ipc_mqueue ips_messages;
};

注意到上面这两个结构体的第一个字段都是 struct ipc_object 字段

因此当 ipc_entry 中的 ie_object 指针指向这两个结构体中的 ipc_object 结构体字段时,这种指向关系也等价于直接指向这两个结构体的基地址

5. mach_port_t 与 mach_port_name_t

注:这一节较为重要。

在用户层调用 mach API 时,我们经常会看到 mach_port_tmach_port_name_t 类型,并很容易将这些类型混淆(至少我学 mach 的时候经常混)。

引起混淆的原因很简单

  1. 用户层输出这两个类型的值都是同一个整型数值
  2. 使用某些 mach API 时,经常将 mach_port_t 类型的值直接作为 mach_port_name_t 类型的函数参数。
  3. 被一些函数声明给混淆了。明明是mach_port_t类型的参数,偏偏参数名为 name

虽然这两个类型在用户层中表示的值是相同的,但实际上在内核里有着非常明显的不同。

  • 对于端口名称 (port name, aka mach_port_name_t) 来说,port name 只是表示特定于某个 task 的 port,并且不携带任何关于该 port 的 right 相关信息

  • 而对于 端口 (port, aka mach_port_t) 来说,它表示的是可以添加或删除某些端口权限一个引用。当内核返回这样的一个引用给用户层时,用户层所获取到的是这个引用的 name,即 port name。这就是为什么用户层中,内核返回的 mach_port_name_t 和 mach_port_t 类型的变量都是同一个整型值。

    正常来说,对于某个 mach port 来说,引用不同 right 的 name互不相同的。但也有例外,下文中有说明。

    但需要注意的是 ,mach_port_t 类型在内核中,确确实实映射了一个 ipc_port 类型的结构体,其中该结构体内含 port right 的相关数据。但 mach_port_name_t 只是 mach port 的一个整数表示形式,没有映射任何 ipc_port 类型的结构体,因此就没有关于该 mach port 的 right 信息。

同时还有一点需要注意:对于某一个特定 mach port(即引用了相同的 ipc_port 结构体) ,如果该端口有多个 right,例如同时拥有 send right 和 receiver right。那么这些 right 的 name 将合并成一个 name,即一个 name 可以同时代表目标 mach port 的 send right 和 receiver right。但是,send once right 所对应的 name 总是唯一的命令,即总是会有一个独立的 name 来指代这个 mach port 的 send right。

当这两个类型被很好的区分开后,mach_port_t、mach_port_name_t、mach port right 以及 mach port 之间的关系就能很好的区分开了,对理解 mach IPC 有着非常多的帮助。这里先完整概括一下 port、right 以及 name 之间的关系:

  • 我们常常说的 mach port,指代的是内核中的 ipc_port 结构体,我们可以向这个 mach port 发送信息以及接收信息。

  • 一个 mach port 在一些 task 中可能存在一些 rights,这些 rights 指定了当前 task 对该 mach port 的一些权限,例如接收信息,发送信息权限等等。这些在当前 task存在权限的 mach port ,一定在当前 task 的 ipc_space 中存在一个 ipc_port 结构体

    因此,mach_port_t 类型在内核(注意不是用户层)中就指代了一个在当前 task 中的 mach port 的一个 right 引用。

    注意 mach_port_t 类型在内核中不是直接代表一个 mach port,是不是觉得很绕?

  • mach_port_name_t 类型在内核层和用户层只是表示了一个 mach port,并没有涉及任何 right,也就更别说是 right 的引用了。

  • 当内核返回给用户层一个 mach_port_t 类型引用时,与内核不同,这里用户层接收到的值的实质对应该 right 的 name。即在用户层中, mach_port_t 类型的值表示的是对某个 mach port 对应的 rightname(注意此时并非直接引用 right)。因此 mach_port_t 类型的值和 mach_port_name_t 类型的值会是相同的。

  • 承接刚刚说的,正常来讲一个 mach_port_t 类型的值在用户层中会是某个 mach port 中一个 right 的 name。

    但是,如果某个 mach_port_t 已经表示了某个 mach port 的 send right name,那么当用户请求一个表示了某个 mach port 的 receive right name(注意两个 right 是不同类型的)。那么这次请求将重用之前的 send right name,也就是说最后这个 port 既表示 send right name 又表示 receive right name。

    这种机制称为名称合并,即不同类型的 right 的 name 将可以合并为一个 name ,并指定多个 right。但需要注意的是 send-once right name无法被合并

    例如两个 mach_port_t 类型分别表示引用某个 mach port 的 send rightsend-once right 的 name,那么此时这两个 mach_port_t 类型的变量将是不同值。

七、部分 IPC 基础 API

1. User Mode

a. mach_port_names

作用:返回指定 task 相关的 port namespace 信息。

函数定义如下:

1
2
3
4
5
6
kern_return_t   mach_port_names
(ipc_space_t task,
mach_port_name_array_t *names,
mach_msg_type_number_t *namesCnt,
mach_port_type_array_ *types,
mach_msg_type_number_t *typesCnt);

其中,

  • task:待查阅的 task port,查阅者必须拥有目标 task 的 mach port send right。
  • names:存放查询结果的 mach_port_name_t 数组
  • namesCnt:names 数组中的元素个数
  • types:存放对于 names 数组中每个对应 name 的 right 类型的数组。
  • typesCnt:types 数组中的元素个数。

可以肯定的是,namesCnt 应该等于 typesCnt

而这个接口返回两个单独的 Cnt 是因为这是 Mach Interface Generator 的产物。‘

需要注意的是,names 和 types 的缓冲区将会被自动创建,因此在使用完成后需要及时调用 vm_deallocate 释放。

b. mach_port_get_attributes

作用:查询指定 port 的相关信息。

函数定义:

1
2
3
4
5
6
kern_return_t   mach_port_get_attributes
(ipc_space_t task,
mach_port_name_t name,
mach_port_flavor_t flavor,
mach_port_info_t port_info,
mach_msg_type_number_t *port_info_count);

其中,参数说明如下:

  • task:持有待查询 port 的 task

  • name:待查询 port 的 name

  • flavor:所查询的信息类型

    查询的信息类型有两种,分别是:

    • MACH_PORT_LIMITS_INFO:返回端口的资源限制(mach_port_limits
    • MACH_PORT_RECEIVE_STATUS:随机返回与端口相关的 right 和 message 的信息(mach_port_status
  • port_info:一个指向存放查询结果的缓冲区的指针

  • port_info_count:缓冲区最大可存放结果的数量。函数返回时该值将会被修改为实际返回的查询结果个数。

以下是组合使用上面两个函数的一个简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <stdio.h>
#include <stdlib.h>
#include <mach/mach.h>

#define EXIT_ON_MACH_ERROR(msg, retval) \
if (kr != KERN_SUCCESS) { mach_error(msg ":", kr); exit((retval)); }

void print_mach_port_type(mach_port_type_t type)
{
if (type & MACH_PORT_TYPE_SEND) printf("SEND ");
if (type & MACH_PORT_TYPE_RECEIVE) printf("RECEIVE ");
if (type & MACH_PORT_TYPE_SEND_ONCE) printf("SEND_ONCE ");
if (type & MACH_PORT_TYPE_PORT_SET) printf("PORT_SET ");
if (type & MACH_PORT_TYPE_DEAD_NAME) printf("DEAD_NAME ");
if (type & MACH_PORT_TYPE_DNREQUEST) printf("DNREQUEST ");
printf("\n");
}

int main(int argc, char **argv)
{
int i;
pid_t pid;
kern_return_t kr;
mach_port_name_array_t names;
mach_port_type_array_t types;
mach_msg_type_number_t ncount, tcount;
mach_port_limits_t port_limits;
mach_port_status_t port_status;
mach_msg_type_number_t port_info_count;
task_t task;
task_t mytask = mach_task_self();

if (argc != 2)
{
fprintf(stderr, "usage: %s <pid>\n", argv[0]);
exit(1);
}

pid = atoi(argv[1]);
kr = task_for_pid(mytask, (int)pid, &task);
EXIT_ON_MACH_ERROR("task_for_pid", kr);

// retrieve a list of the rights present in the given task's IPC space,
// along with type information (no particular ordering)
kr = mach_port_names(task, &names, &ncount, &types, &tcount);
EXIT_ON_MACH_ERROR("mach_port_names", kr);

printf("%8s %8s %8s %8s %8s task rights\n",
"name", "q-limit", "seqno", "msgcount", "sorights");
for (i = 0; i < ncount; i++)
{
printf("%08x ", names[i]);

// get resource limits for the port
port_info_count = MACH_PORT_LIMITS_INFO_COUNT;
kr = mach_port_get_attributes(
task, // the IPC space in question
names[i], // task's name for the port
MACH_PORT_LIMITS_INFO, // information flavor desired
(mach_port_info_t)&port_limits, // outcoming information
&port_info_count); // size returned
if (kr == KERN_SUCCESS)
printf("%8d ", port_limits.mpl_qlimit);
else
printf("%8s ", "-");

// get miscellaneous information about associated rights and messages
port_info_count = MACH_PORT_RECEIVE_STATUS_COUNT;
kr = mach_port_get_attributes(task, names[i], MACH_PORT_RECEIVE_STATUS,
(mach_port_info_t)&port_status,
&port_info_count);
if (kr == KERN_SUCCESS)
{
printf("%8d %8d %8d ",
port_status.mps_seqno, // current sequence # for the port
port_status.mps_msgcount, // # of messages currently queued
port_status.mps_sorights); // # of send-once rights
}
else
printf("%8s %8s %8s ", "-", "-", "-");
print_mach_port_type(types[i]);
}

vm_deallocate(mytask, (vm_address_t)names, ncount * sizeof(mach_port_name_t));
vm_deallocate(mytask, (vm_address_t)types, tcount * sizeof(mach_port_type_t));

exit(0);
}

示例效果:

image-20211231141800537

c. mach_port_request_notification

当某个 mach port 被销毁后,其他 task 所持有的 right 都将转变为 dead name,因此当发送信息时,发送者可以得知目标 mach port 被销毁。

但如果发送者希望目标 mach port 在被销毁时能立即通知发送者,而不是等到发送者发送数据时才得知,那么这就是 mach_port_request_notification 函数的作用。该函数指定目标 mach port 的事件请求通知。以下是该函数的声明:

1
2
3
4
5
6
7
8
kern_return_t   mach_port_request_notification
(ipc_space_t task,
mach_port_name_t name,
mach_msg_id_t variant,
mach_port_mscount_t sync,
mach_port_send_once_t notify,
mach_msg_type_name_t notify_type,
mach_port_send_once_t *previous);

具体参数暂不说明,等实际应用到了再来补充。

2. Kernel Mode

a. ipc_entry_lookup

注:ipc_right_lookup_write 是该函数的 Wrapper;而 ipc_right_lookup_read 又是 ipc_right_lookup_write 的宏。

功能:在当前 taskIPC space 结构体中,根据传入的用户层 mach port name,获取到内核中对应的 ipc_entry_t 结构

先上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ipc_entry_t
ipc_entry_lookup(
ipc_space_t space,
mach_port_name_t name)
{
mach_port_index_t index;
ipc_entry_t entry;

assert(is_active(space));
// 获取 name 所对应的 index
index = MACH_PORT_INDEX(name);
if (index < space->is_table_size) {
entry = &space->is_table[index];
if (IE_BITS_GEN(entry->ie_bits) != MACH_PORT_GEN(name) ||
IE_BITS_TYPE(entry->ie_bits) == MACH_PORT_TYPE_NONE) {
entry = IE_NULL;
}
}
else {
entry = IE_NULL;
}

assert((entry == IE_NULL) || IE_BITS_TYPE(entry->ie_bits));
return entry;
}

在 ipc_entry_lookup 函数中,我们可以看到,mach_port_name_t (aka unsigned int) 被分为了2个部分,分别是 MACH_PORT_INDEX 与 MACH_PORT_GEN。组装方式如下所示:

1
2
3
4
#define MACH_PORT_INDEX(name)       ((name) >> 8)
#define MACH_PORT_GEN(name) (((name) & 0xff) << 24)
#define MACH_PORT_MAKE(index, gen) \
(((index) << 8) | (gen) >> 24)

其中,

  • MACH_PORT_INDEX 用于在 task->ipc_space->is_table 中充当索引作用,有点类似于文件描述符。
  • MACH_PORT_GEN 说明当前 mach port 是第几代(generation)的。个人猜测这是为了将 mach port 与过去那些相同 index 但不同(且已经被释放)的 mach port 所区分开,防止混淆。

还有个需要注意的地方是,在 mach_port_name_t 中,其32位数据的用途划分如下:

1
2
3
4
+--------------------+-----+
| is_table index | gen |
+--------------------+-----+
32 8 0

但在 ipc_entry 结构体中的 ie_bits 字段,其32位数据用途如下所示:

1
2
3
4
+-----+-----+------+----------------+
| gen | | type | user-reference |
+-----+-----+------+----------------+
32 24 21 16 0

b. ipc_right_copyin

先简单了解一下函数命名规则:

  • xxx_copyin:发送方调用
  • xxx_copyout:接收方调用

ipc_right_copyin 会根据传入的 msgt_name (mach_msg_type_name_t) ,对目标 ipc_entry_t 中的 ipc_port 结构体上的某些字段进行修改操作,并返回对应的 ipc_port 结构体指针给上层调用者

回顾一下上面 ipc_port 结构体的字段,该函数主要会对这三个字段进行增加操作:

还有些其他的我没贴上来。

1
2
3
mach_port_mscount_t ip_mscount; // make send 的次数
mach_port_rights_t ip_srights; // send right 当前存在的发送权限的数量
mach_port_rights_t ip_sorights; // send once right 数量

该函数涉及到 mach port 的权限操作。port right 类型主要有以下几种:

1
2
3
4
5
6
7
8
9
10
#define MACH_MSG_TYPE_MOVE_RECEIVE      16    /* Must hold receive right */
#define MACH_MSG_TYPE_MOVE_SEND 17 /* Must hold send right(s) */
#define MACH_MSG_TYPE_MOVE_SEND_ONCE 18 /* Must hold sendonce right */
#define MACH_MSG_TYPE_COPY_SEND 19 /* Must hold send right(s) */
#define MACH_MSG_TYPE_MAKE_SEND 20 /* Must hold receive right */
#define MACH_MSG_TYPE_MAKE_SEND_ONCE 21 /* Must hold receive right */
#define MACH_MSG_TYPE_COPY_RECEIVE 22 /* NOT VALID */
#define MACH_MSG_TYPE_DISPOSE_RECEIVE 24 /* must hold receive right */
#define MACH_MSG_TYPE_DISPOSE_SEND 25 /* must hold send right(s) */
#define MACH_MSG_TYPE_DISPOSE_SEND_ONCE 26 /* must hold sendonce right */

这个函数我们暂时不用深入了解,只需知道该函数除了做一些 right 处理以外,还会将 ipc_entry 中的 ipc_port 结构体返回给调用者即可。

c. port_name_to_task

功能:在内核空间中,根据用户传入的 task port name (一串数字表示的值),获取所实际引用的 task 结构体指针。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
task_t
port_name_to_task(
mach_port_name_t name)
{
ipc_port_t kern_port;
kern_return_t kr;
task_t task = TASK_NULL;

if (MACH_PORT_VALID(name)) {
kr = ipc_object_copyin(current_space(), name,
MACH_MSG_TYPE_COPY_SEND,
(ipc_object_t *) &kern_port);
if (kr != KERN_SUCCESS)
return TASK_NULL;

task = convert_port_to_task(kern_port);

if (IP_VALID(kern_port))
ipc_port_release_send(kern_port);
}
return task;
}

该函数内部会将 task port name 传入 ipc_object_copyin 函数中,获取其对应的 task port 的 ipc_port 结构体。之后,在 convert_port_to_task 中,将 task port 对应的 ipc_port 结构体中的 ip_kobject 字段的值取出,并作为 目标 task 结构体指针。

d. mach_msg

mach_msg 是用户用于发送和接受 mach message 的 API。

上个完整的流程图:

image-20211231144049005

mach_msg_overwrite_trap 是 mach msg 发送与接收消息的实际内核处理函数。该函数的实现分为两部分,分别是发送消息和接收消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
mach_msg_return_t
mach_msg_overwrite_trap(
struct mach_msg_overwrite_trap_args *args)
{
mach_vm_address_t msg_addr = args->msg;
mach_msg_option_t option = args->option;
mach_msg_size_t send_size = args->send_size;
mach_msg_size_t rcv_size = args->rcv_size;
mach_port_name_t rcv_name = args->rcv_name;
mach_msg_timeout_t msg_timeout = args->timeout;
mach_msg_priority_t override = args->override;
mach_vm_address_t rcv_msg_addr = args->rcv_msg;
__unused mach_port_seqno_t temp_seqno = 0;

mach_msg_return_t mr = MACH_MSG_SUCCESS;
vm_map_t map = current_map();

/* Only accept options allowed by the user */
option &= MACH_MSG_OPTION_USER;

if (option & MACH_SEND_MSG) { /* ... ipc_kmsg_send(xxx) ... */ }
if (option & MACH_RCV_MSG) { /* ... ipc_mqueue_receive_on_thread(xxx) ... */ }

return MACH_MSG_SUCCESS;
}

ipc_kmsg_t 结构体即待发送的内核消息,结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ipc_kmsg {
mach_msg_size_t ikm_size;
struct ipc_kmsg *ikm_next; /* next message on port/discard queue */
struct ipc_kmsg *ikm_prev; /* prev message on port/discard queue */
mach_msg_header_t *ikm_header; // 指向 Mach Message 的指针
ipc_port_t ikm_prealloc; /* port we were preallocated from */
ipc_port_t ikm_voucher; /* voucher port carried */
mach_msg_priority_t ikm_qos; /* qos of this kmsg */
mach_msg_priority_t ikm_qos_override; /* qos override on this kmsg */
struct ipc_importance_elem *ikm_importance; /* inherited from */
queue_chain_t ikm_inheritance; /* inherited from link */
sync_qos_count_t sync_qos[THREAD_QOS_LAST]; /* sync qos counters for ikm_prealloc port */
sync_qos_count_t special_port_qos; /* special port qos for ikm_prealloc port */
#if MACH_FLIPC
struct mach_node *ikm_node; /* Originating node - needed for ack */
#endif
};

该结构体中包含了较多字段,其中存在一个指向待发送 Mach message 的指针。

受限于知识储备,内核中的具体细节留待更进一步的分析。

八、MIG

1. 概述

一说到 Mach IPC 后,一个不得不提到的东西便是 MIG(Mach Interface Generator)。但这里我们不过多了解 MIG 中非常具体的使用方式与编写语法,只简单了解一下它的功能与意义等等。

通过上面的例子我们可以知道,Mach IPC 可以用与 **RPC(远程过程调用)**中。通俗的讲,它可以做到:当 Client ”调用“ 某个远程方法时,Server 将从 Mach IPC 中收到信息并实际执行该方法,最后将调用结果再通过 Mach IPC 返回给 Client,以实现 Client 的透明调用。

那么如果 Client 需要调用的方法很多,那对于开发者而言,除了需要完成方法的实际实现以外,他们还得手工完成 Mach IPC 之间的信息处理与分发等等重复乏味且机械的工作,开发效率极低。

因此, MIG 的使用可以帮助我们完成后者,解放生产力,让开发人员更关注于方法的实现。

MIG 可以从用户编写的 RPC 规范文件(.defs 文件)中,生成出 CS 架构的代码。这些代码将自动完成 Mach Message 的准备、发送、接收、解包等等功能。同时由于代码是自动生成的,因此可以提高代码一致性,降低代码发生错误的可能。

MIG 将会生成三个文件,分别是

  1. 用于用户 include 的一个头文件
  2. client 端的一个源文件,用于和 client 的其他代码所链接。
  3. server 端的一个源文件,用于和 server 端的其他代码所链接。这部分代码会自动完成消息接收,事件分发,函数调用,信息回复等操作。

以下是一个示例:
image-20220105175226555

2. CS 架构程序示例

a. 概述

这部分我们将简单了解一下如何使用 MIG 创建一个简单的 CS 程序。

在这个 CS 架构项目中,Server 程序会提供两个接口 :

  • string_length:获取传入字符串的长度
  • factorial:计算传入数字的阶乘

该示例来自于:*OS Internal Vol 1

b. 杂项公共头文件

首先,给出 Client 和 Server 的杂项公共头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// misc_types.h 

#ifndef _MISC_TYPES_H_
#define _MISC_TYPES_H_

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>

// The server port will be registered under this name.
#define MIG_MISC_SERVICE "MIG-miscservice"

// Data representations
typedef char input_string_t[64];
typedef int xput_number_t;

typedef struct {
mach_msg_header_t head;

// The following fields do not represent the actual layout of the request
// and reply messages that MIG will use. However, a request or reply
// message will not be larger in size than the sum of the sizes of these
// fields. We need the size to put an upper bound on the size of an
// incoming message in a mach_msg() call.
NDR_record_t NDR;
union {
input_string_t string;
xput_number_t number;
} data;
kern_return_t RetCode;
mach_msg_trailer_t trailer;
} msg_misc_t;

xput_number_t misc_translate_int_to_xput_number_t(int);
int misc_translate_xput_number_t_to_int(xput_number_t);
void misc_remove_reference(xput_number_t);
kern_return_t string_length(mach_port_t, input_string_t, xput_number_t *);
kern_return_t factorial(mach_port_t, xput_number_t, xput_number_t *);

#endif // _MISC_TYPES_H_

在这个头文件中,定义了两个类型 input_string_txput_number_t,并声明了一些函数。

在这些函数中,有两个是目标接口声明,另外3个是 MIG 生成的代码内部会调用到的,一会再说明。

其中的 msg_misc_t 结构体声明只用于 Server 调用 mach_msg_server 函数时指定最大 message 长度,不会实际实例化该结构体。

c. RPC defs

之后,再给出 defs 文件:

defs 文件中的一些符号说明,已经以注释的形式写入 defs 中,下面不再赘述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/* 
* A "Miscellaneous" Mach Server
*/

/*
* File: misc.defs
* Purpose: Miscellaneous Server subsystem definitions
*/

/*
* Subsystem identifier
* 指定当前的 mig 中的接口ID 从 500 开始,同时该文件所生成的模块均以 `misc` 命名
* 这里的字符串也会影响到输出的 `*Server.c`、 `*User.c` 等文件的命名
*/
Subsystem misc 500;

/*
* Type declarations
* 类型规范部分:用于定义函数调用参数的数据类型
* MIG支持简单类型、结构化类型、指针类型和多态类型的声明。
*/
#include <mach/std_types.defs>
#include <mach/mach_types.defs>

type input_string_t = array[64] of char;
/*
* 这里可能要稍微说明一下
* 首先,设置 xput_number_it 的类型为 int
* InTran 指定当函数传入 int 时,如果需要将其转换成 xput_number_t 类型,则调用 misc_translate_int_to_xput_number_t 函数转换
* OutTran 指定当函数需要输出 int 时,如果需要将其从 xput_number_t 类型转换,则调用 misc_translate_xput_number_t_to_int 函数来转换
* Destructor 指定当 xput_number_t 类型的变量需要析构时,执行该函数
*/
type xput_number_t = int
CType : int
InTran : xput_number_t misc_translate_int_to_xput_number_t(int)
OutTran : int misc_translate_xput_number_t_to_int(xput_number_t)
Destructor : misc_remove_reference(xput_number_t)
;

/*
* Import declarations
*/
import "misc_types.h";

/*
* Operation descriptions
* 需要注意的是,每个函数声明中,至少要包含一个 mach_port_t 类型的参数。
* 一方面,在 Client 中,这个参数指定了向哪个 Server 发起调用
* 而另一方面,Server 中具体方法的实现也可以获取到一个 mach_port_t 类型的值,从而判断调用者
*/

/* This should be operation #500 */
routine string_length(
server_port : mach_port_t;
in instring : input_string_t;
out len : xput_number_t);
/* Create some holes in operation sequence */
// 跳过序列中的 501、502、503,这里的 skip 操作可以保持接口的兼容性,有点类似于 protobuf
Skip;
Skip;
Skip;

/* This should be operation #504, as there are three Skip's */
routine factorial(
server_port : mach_port_t;
in num : xput_number_t;
out fac : xput_number_t);

/*
* Option declarations
* 这里设置了两个 Prefix,这些 Prefix 会分别作为所调用的/所实现的 IPC 操作函数名称前缀
*/
ServerPrefix Server_;
UserPrefix Client_;

更多 MIG defs 语法可以参照 Using Mach Messages - NeXTstep 3.3 Developer Documentation

d. Server

Server 源程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// server.c 

#include <string.h>
#include "misc_types.h"

extern boolean_t misc_server(mach_msg_header_t *inhdr,
mach_msg_header_t *outhdr);

// InTran
xput_number_t
misc_translate_int_to_xput_number_t(int param) {
printf("misc_translate_incoming(%d)\n", param);
return (xput_number_t)param;
}

// OutTran
int
misc_translate_xput_number_t_to_int(xput_number_t param) {
printf("misc_translate_outgoing(%d)\n", (int)param);
return (int)param;
}

// Destructor
void
misc_remove_reference(xput_number_t param) {
printf("misc_remove_reference(%d)\n", (int)param);
}

// an operation that we export
kern_return_t
string_length(mach_port_t server_port,
input_string_t instring,
xput_number_t *len)
{
if (!instring || !len)
return KERN_INVALID_ADDRESS;

*len = strlen(instring);

return KERN_SUCCESS;
}

// an operation that we export
kern_return_t
factorial(mach_port_t server_port, xput_number_t num, xput_number_t *fac) {
if (!fac)
return KERN_INVALID_ADDRESS;

*fac = 1;
for (int i = 2; i <= num; i++)
*fac *= i;

return KERN_SUCCESS;
}

int main(void) {
kern_return_t kr;
mach_port_t server_port;

if ((kr = bootstrap_check_in(bootstrap_port, MIG_MISC_SERVICE,
&server_port)) != BOOTSTRAP_SUCCESS) {
mach_port_deallocate(mach_task_self(), server_port);
mach_error("bootstrap_check_in:", kr);
exit(1);
}

mach_msg_server(misc_server, // call the server-interface module
sizeof(msg_misc_t), // maximum receive size
server_port, // port to receive on
MACH_MSG_TIMEOUT_NONE); // options
return 0;
}

Server 端要做的事情稍微多一点:

  1. 程序执行时,Server 将 server port 注册进 bootstrap 中。

    初次之外,Server 还执行 mach_msg_server 函数,使当前进程一直循环处理 Mach Message。

    mach_msg_server 函数的第一个参数指定了 MIG 生成的 misc_server 处理例程,该例程会根据传进的 Mach Message 执行指定的接口。

  2. Server 端实现了两个接口的具体实现。当 Server 接收到 Client 端发来的信息时,这两个方法将在 miscServer.c 中被调用。

  3. 除此之外,Server 还实现了其他 MIG 中会调用的函数。

e. Client

Client 源程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// client.c 

#include "misc_types.h"

#define INPUT_STRING "Hello, MIG!"
#define INPUT_NUMBER 5

int
main(int argc, char **argv)
{
kern_return_t kr;
mach_port_t server_port;
int len, fac;

// look up the service to find the server's port
if ((kr = bootstrap_look_up(bootstrap_port, MIG_MISC_SERVICE,
&server_port)) != BOOTSTRAP_SUCCESS) {
mach_error("bootstrap_look_up:", kr);
exit(1);
}

// call a procedure
if ((kr = string_length(server_port, INPUT_STRING, &len)) != KERN_SUCCESS)
mach_error("string_length:", kr);
else
printf("length of \"%s\" is %d\n", INPUT_STRING, len);

// call another procedure
if ((kr = factorial(server_port, INPUT_NUMBER, &fac)) != KERN_SUCCESS)
mach_error("factorial:", kr);
else
printf("factorial of %d is %d\n", INPUT_NUMBER, fac);

mach_port_deallocate(mach_task_self(), server_port);

exit(0);
}

Client 源码较短,只做了两件事:

  1. 向 bootstrap 查询 Server 注册的 server port

  2. 向 server port 调用 string_lengthfactorial 方法,需要注意到这两个方法的第一个参数均为 mach_port_t 类型,且方法的实现位于 miscUser.c

    为什么这两个方法的实现位于 miscUser.c 中而不是 server.c 中?

    因为对于 Client 端来说,两个方法的实际实现不归 Client 端来管,miscUser.c 中的两个同名函数最终会执行 mach IPC 向 Server 发起请求。

f. 编译与运行

使用以下命令编译并运行:

1
2
3
4
5
6
7
8
# 终端1
mig -v misc.defs
gcc -Wall -g -o server server.c miscServer.c
gcc -Wall -g -o client client.c miscUser.c
./server

# 终端2
./client

这是所有源文件的关联图:

image-20220106000421902

运行结果:

image-20220105234905893

这是 Client 和 Server 的关系:

image-20220106000500793

九、参考

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2022 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~