叁: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開源代碼