叄:RunLoop中的訊息傳遞機制

語言: CN / TW / HK

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中的訊息,定義在檔案中,簡單來說,一個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 就是一個雙向連結串列的節點, 原始碼如下:

```c 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開源程式碼