叁:RunLoop中的消息传递机制

1,641 阅读10分钟

iOS系统的历史

Mac OS X融合了Mac OS Classic和NextStep的优点:Mac OC Classic的GUI以及NextStep的架构。

全新的Mac OS X在设计与实现上都和NextStep非常接近,诸如Cocoa、Mach、Interface Builder等核心组件都源于NextStep。

iOS最初被称为iPhone OS,它是OS X对应移动平台的分支,本质上iOS就是Mac OS X。而iOS也是iPad OS,tvOS,watchOS这三者的基础。正因为本质上iOS就是Mac OS X,所以iOS拥有和和Mac OS一样的操作系统层次结构以及相同的操作系统核心Dawin。

iOS系统的架构

Apple在关于OS X以及iOS系统架构的文档中,展示了非常简洁的分层,某种意义上,甚至有些过于简单

  • The User ExperienceLayer(用户UI层)

包括Aqua,Dashboard,Spotlight以及一些特性。在iOS中,用户体验完全取决于SpringBoard,同时, iOS中Spotlight也是支持的。

  • The Application Frameworks layer(应用框架层)

包括Cocoa,Carbon以及Java。然而在iOS中,只有Cocoa(严格来讲,Cocoa Touch,Cocoa的派生物)

  • The Core Frameworks(核心框架层)

有时也被称为图形和媒体层(Graphic and Media layer)。包括核心框架,Open GL以及Quick Time。

  • Darwin(系统核心层)

操作系统核心——kernel以及UNIX shell的环境。

在以上的这些层级中,Darwin是完全开源的,而顶部的其他层级都是闭源的,Apple保持专利。iOS 和 Mac OS整体上是非常像的,但是还是有一些细微的不同。比如iOS使用的是Spring Board而OS X使用的是Aqua,因为前者是针对触屏操作,而后者针对的是鼠标操作。如果深入的看看Darwin,可以得到如下结构:

image.png

要明确的是Darwin的核心是XNU内核。它是一个混合内核,将宏内核和微内核两者的特点兼收并蓄: 比如为微内核中提高操作系统模块化程度,以及内存保护和消息传递的机制;还有宏内核在高负荷下表现的高性能。XNU主要是由Mach,BSD,以及IOKit组成的。

上面这张图提出了一个问题:在什么时候会发生用户态和内核态的切换? 用户态和内核态的区分是非常明显的,但是应用会频繁去使用内核服务,所以这两种态(用户态和内核态)之间的转换就需要一种高效的 且安全的方式。在XNU内核中用户态和内核态的切换有两种情况: **其一是主动切换:当应用需要内核服务的时候,它会发起对内核态的调用。通过预先设定好的硬件指令,从用户态到内核态的转换就会发生。这些服务称为*system calls。 其二是被动切换:***当某个执行异常,中断等发生时,代码的执行就会被暂停。控制权就会转移给内核态的错误预处理机制或者中断路由服务(ISR:interrupt service routine)

XNU主要的核心其实是Mach,它作为微内核,只处理操作系统最基础的一些职责,提供了进程和线程的抽象、虚拟内存的管理、任务调度、**进程间通信(IPC)**这些基本的功能。而XNU暴露给用户的是BSD层,这一层对下在一些底层的功能上使用了Mach,对上,它给应用提供了流行的POSIX API,这也使得OSX系统对于许多其他的UNIX实现是兼容的。

Mach只具备有限的API,它并不是要成为一个五脏俱全的操作系统,它只是提供一些基本的功能,没有它,那么操作系统也无法工作。而一些其他的功能诸如文件管理以及设备访问,都是由它的上一层也就是BSD层来处理的,这一层提供了一些更高层级的抽象,比如The POSIX线程模型(Pthread),文件系统,网络等功能。

Mach

Mach拥有一个很简单的概念:一个最小的核心支持一个面向对象的模型,其中的子系统通过Message相互通信。其他的操作系统都是提供了一个完整的模型,而Mach提供了一个基本的模型,可以在此基础上实现操作系统本身,OS X的XNU是Mach之上的一个特殊实现。

**在Mach中,一切都被视为对象。**进程(Mach中称为tasks),线程以及虚拟内存都是对象,每一个都有它的属性。但是这个并不是值得大书特书的地方,因为其他的操作系统也可以使用对象来实现。真正让Mach不同的是它选择通过消息传递(Message Passing)来实现对象之间的通信。

所以Mach最基础的概念就是两个端点(Port)中交换的**message,**这就是Mach的IPC(进程间通信)的核心。

Mach中的消息,定义在<mach/message.h>文件中,简单来说,一个message就是msgh_size大小的blob, 带着一些flags,从一个端口发送到另一个端口。

typedef struct {
    mach_msg_header_t header;
    mach_msg_body_t   body;

} mach_msg_base_t;

// 消息头是必须的,它定义了一个消息所需要的数据
typedef struct {
    mach_msg_bites_t   msgh_bits;
    mach_msg_size_t    msgh_size;
    mach_port_t        msgh_remote_port;
    mach_port_t        msgh_local_port;
    mach_msg_size_t    msgh_reserved;
    mach_msg_id_t      msgh_id;
} mach_msg_header_t;

Mach Message发送和接收消息都使用了同样的API:mach_msg()。**这个方法在用户态和内核态都有实现。**它通过option参数来决定是收消息,还是发消息。

mach_msg_return_t mach_msg(mach_msg_header_t            msg,f
                           mach_msg_option_t            option,
                           mach_msg_size_t              send_size,
                           mach_msg_size_t              reveive_limit,
                           mach_port_t                  reveive_name,
                           mach_msg_timeout_t           timeout,
                           mach_port_t                  notify,
                           );

在发送消息或者接收消息的时候,在用户态中Mach message使用了mach_msg() ,它会通过内核的Mach trap机制调用对应的内核方法mach_msg_trap(),。而这个mach_msg_trap()会调用到mach_msg_overwrite_trap(), 这个方法通过MACH_SEND_MSG或者是MACH_RCV_MSG的flag来决定是发送操作,还是接受操作。

image.png

具体关于mach_msg_trap()如何工作的,可以看Apple开源的xnu中关于mach的源码。同时本文中的大量关于系统和架构中的知识点均参考自《Mac OS X and iOS Internals To the Apples Core》。

RunLoop接受消息

接下来我们回到RunLoop,首先问一个问题:RunLoop中是如何实现被唤醒的呢?

从源码中可知,在RunLoop即将进入休眠状态之后,它会调用**CFRunLoopServiceMachPort()方法,而这个方法内部会调用mach_msg()方法。所以RunLoop的唤醒就是通过mach_msg()**方法来接受port或者port set的消息,被唤醒后接着再处理相应的任务。以下是这两个方法的定义:

static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port,
                                          mach_msg_header_t **buffer,
                                          size_t buffer_size,
                                          mach_port_t *livePort,
                                          mach_msg_timeout_t timeout,
                                          voucher_mach_msg_state_t *voucherState,
                                          voucher_t *voucherCopy);

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        receive_limit,
                     mach_port_t             receive_name,
                     mach_msg_timeout_t           timeout,
                     mach_port_t                   notify);

mach_msg方法上述已经提到过了,它既用于发送消息,也用于接受消息。而在Runloop的这个实际应用场景下,它只用于接受消息。以下是对这个方法中的各个参数含义的解释:

msg: 是mach_msg用于发送和接受消息的消息缓冲区

option: Message的options是位值,按位或来结合。应该使用MACH_SEND_MSG和MACH_RCV_MSG中的一种或两种。

send_size: 当发送消息时,指定要发送的message buffer的大小。否则就是零。

receive_limit: 当接受消息时,指定接受的message buffer的大小。否则就是零。

receive_name:当接受消息时,指定了端口或者端口集。消息就是从receive_name指定的端口中接受的。否则就是MACH_PORT_NULL。

timeout:当使用MACH_SEND_TIMEOUT或者MACH_RCV_TIMEOUT选项时,指定放弃前需要等待的时间(单位为毫秒),否则就是MACH_MSG_TIMEOUT_NONE。

notify: 当使用MACH_SEND_CANCEL,MACH_RCV_NOTIFY和MACH_SEND_NOTIFY选项时,指定用于notification的端口。否则就是MACH_PORT_NULL

mach_msg调用用于接受和发送mach消息,它是用相同的缓冲区去来发送和接受消息,也就是msg参数对应的消息缓冲区。

typedef struct {
    mach_msg_bites_t   msgh_bits;
    mach_msg_size_t    msgh_size;
    mach_port_t        msgh_remote_port;
    mach_port_t        msgh_local_port;
    mach_msg_size_t    msgh_reserved;
    mach_msg_id_t      msgh_id;
} mach_msg_header_t;

消息接收

当接受消息的时候,实际上是使来着端口的消息出消息队列。receive_name指定了要从中接受消息的端口或者端口集。

如果指定了端口(port),那么调用者必须拥有该端口的权限,并且该端口不能是端口集的成员。如果没有任何消息,那么调用会被阻塞,根据MACH_RCV_TIMEOUT选项来决定放弃等待的时机。

如果指定了端口集(port set),那么调用者将接收到发送到任何端口成员的消息。端口集没有成员是允许的,并且可以在端口集接收的过程中添加和删除端口。而接收到的消息头中的magh_local_port字段指定消息来着端口集中的哪个端口。

接下来我们再回到RunLoop中的源码调用中,来看这个方法的调用:

// ** 首先是外层
// 1、处理Source1事件的时候,调用了该方法
CFRunLoopServiceMachPort(dispatchPort,
                         &msg,
                         sizeof(msg_buffer),
                         &livePort,
                         0,
                         &voucherState,
                         NULL)

// 2、进入休眠状态的时候,调用了该方法
CFRunLoopServiceMachPort(waitSet,
                         &msg,
                         sizeof(msg_buffer),
                         &livePort,
                         poll ? 0 : TIMEOUT_INFINITY,
                         &voucherState,
                         &voucherCopy);
// ** 然后是里层
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port,
                                          mach_msg_header_t **buffer,
                                          size_t buffer_size,
                                          mach_port_t *livePort,
                                          mach_msg_timeout_t timeout,
                                          voucher_mach_msg_state_t *voucherState,
                                          voucher_t *voucherCopy) {
    for(;;) {
        ···
        ret = mach_msg(msg,
               MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)| MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0) | MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
               0,
               msg->msgh_size,
               port,
               timeout,
               MACH_PORT_NULL);
        ···
    }
}

端口接收:dispatchPort

也就是说在处理source1事件的时候,需要接受的消息是从dispatchPort端口的消息队列中接受的,而这个端口:dispatchPort = _dispatch_get_main_queue_port_4CF(),所以这里只处理GCD的主队列的事件,同时这里CFRunLoopServiceMachPorttimeout参数为0,这意味着,如果没有收到消息,那它就直接放弃而不会继续等待了,这也符合RunLoop的运行逻辑。

端口集接收:waitSet

而在进入休眠状态时,CFRunLoopServiceMachPortport参数是waitSet,这个参数会传递到内部的mach_msg()函数的receive_name参数,这表明它是从这个端口集中接受消息的。那么waitSet包括哪些端口呢?

在__CFRunLoopRun函数中有:
...
dispatchPort = _dispatch_get_main_queue_port_4CF();
__CFPortSet waitSet = rlm -> _portSet;
CFPortSetInsert(dispatchPort, waitSet);
...

那么rlm中的_portSet呢?在__CFRunLoopFindMode函数中
···
mach_port_t queuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
__CFPortSetInsert(queuePort, rlm->_portSet);
__CFPortSetInsert(rlm->_timerPort, rlm->_portSet);
__CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet);
···

在CFRunLoopAddSource方法中:
CFPortSetInsert(src_port, rlm->_portSet);// source1

至此,我们可以确定Apple关于RunLoop文档中,将RunLoop唤醒的几种事件了:

1、基于Port的source事件

2、timer到时间了

3、runloop要超时了

4、runloop被显式唤醒了

那么RunLoop又是如何判断是由那个Port接受到的消息呢?在CFRunLoopServiceMachPort函数中,当成功接受到消息后,会将livePort赋值为**msg->msgh_local_portmsgh_local_port**就是端口集中接受消息的那个端口,而后RunLoop判断livePort的端口,从而决定处理不同的唤醒事件。

__CFRunLoopRun() {
    ···
    if (MACH_PORT_NULL == livePort) {
        ···
    } else if (livePort == rl->_wakeUpPort) {
        ···
    } else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
        ···
    } else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort){
        ···
    } else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort){
        ···
    } else if (livePort == dispatchPort) {
        ···
    } else {
    }
    ···
}

端口接收的是什么?

上述的描述比较明确的是一个端口接收到到消息是会放在了端口的消息队列中,那么这个消息队列是如何实现的呢?从安卓中的looper中可以看到它们使用了链表来管理这种消息队列的,其实在iOS的xnu(x is not Unix)内核底层也是通过双向链表的方式来关系的消息的,在**mach_msg_overwrite_trap** 方法中接收消息的时候,最后都会将消息存储到**ipc_msg**中,而这个ipc_msg 就是一个双向链表的节点, 源码如下:

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;
	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
};

参考

1、mach_msg

2、Mach Message Call

3、深入理解RunLoop

4、Apple文档《Threading Programming Guide

5、《Mac OS X and iOS Internals To the Apples Core》一书

6、opensource.apple.com开源代码