深入解析Mac OS X & iOS 操作系統(tǒng) 學(xué)習(xí)筆記(十一)

Mach 原語:一切以消息為媒介

XNU 的核心是Mach 微內(nèi)核。 Mach 是 OS X 和 iOS 的核心中的核心。盡管Mach 核心被 BSD 層包裝起來了,而且主要的內(nèi)核接口是標(biāo)準(zhǔn)的POSIX 系統(tǒng)調(diào)用,但是這個Mach 核心具有一組獨特的API和原語。

Mach 設(shè)計原則

Mach 采用的是極簡主義的概念:具有一個簡單最小的核心,支持面向?qū)ο蟮哪P?,使得獨立的具有良好定義的組件(實際上就是子系統(tǒng))可以通過消息的方式互相通訊。在Mach 中,所有的東西都是通過自己的對象實現(xiàn)的。進程(在Mach 中稱為任務(wù))、線程、虛擬內(nèi)存都是對象,所有對象都有自己的屬性。其實對象就是C語言結(jié)構(gòu)體加上函數(shù)指針。Mach 的獨特之處在于選擇了通過消息傳遞的方式實現(xiàn)對象和對象之間的通信。XNU 的“官方”API 是BSD 的POSIX API,蘋果保持Mach絕對的極簡。由于外層具有非常豐富的Cocoa API,所以很多開發(fā)者都根本意識不到Mach的存在。不過,Mach調(diào)用仍然是整個架構(gòu)中最基礎(chǔ)的部分。

Mach 設(shè)計目標(biāo)

Mach的手機文檔列出了一些設(shè)計目標(biāo),其中首要目標(biāo)就是將所有功能移出內(nèi)核,并放在用戶態(tài)中,將內(nèi)核保持在極簡的狀態(tài):

  • “控制點”或執(zhí)行單元(線程)管理
  • 線程或線程組(任務(wù))的資源分配
  • 虛擬內(nèi)存分配和管理
  • 底層物理資源:即CPU、內(nèi)存和任何其他物理設(shè)備的分配

Mach 消息

Mac 中最基本的概念就是消息了,消息在兩個端點(endpoint)或端口(port)之間傳遞。消息是Mach IPC 的核心構(gòu)建塊。Mach 消息的設(shè)計考慮了參數(shù)串行話、對齊、填充(padding,為了對齊)和字節(jié)順序的問題。

發(fā)送消息

Mach 消息的發(fā)送和接收都是通過同一個API函數(shù) mach_msg( )進行的。這個函數(shù)在用戶態(tài)和內(nèi)核中都有實現(xiàn)的。Mach 消息原本是為真正的微內(nèi)核架構(gòu)而設(shè)計的。也就是說,mach_msg( )函數(shù)必須在發(fā)送者和接收者之間復(fù)制消息所在的內(nèi)存。盡管這種實現(xiàn)忠實于微內(nèi)核的范式,但是事實證明頻繁內(nèi)存復(fù)制操作帶來的性能損耗是不能忍受的,因此,XNU 算是通過單一內(nèi)核的方式“作弊”:所有的內(nèi)核組件都共享一個地址空間,因此消息傳遞只需要傳遞消息的指針就可以了,從而省去了昂貴的內(nèi)存復(fù)制操作。
為了實現(xiàn)消息的發(fā)送和接收,mach_msg( ) 函數(shù)調(diào)用了一個Mach 陷阱(trap)。Mach 陷阱就是和系統(tǒng)調(diào)用的概念,在用戶態(tài)調(diào)用mach_msg_trap( ) 會引發(fā)陷阱機制,切換到內(nèi)核態(tài),在內(nèi)核態(tài)中,內(nèi)核實現(xiàn)的mach_msg( ) 會完成實際的工作。

端口

消息在端點(也稱為端口)之間傳遞,端口只不過是32為整型的標(biāo)識符。所有的mach 原生對象都是通過對于的端口訪問的。也就是說,查找一個對象的句柄(handle)時,實際上請求的是這個對象端口的句柄。

深入IPC

IPC 所需要的基本原語:消息、發(fā)送和接收消息的端口,以及確保安全并發(fā)的信號量和鎖。每一個Mach 任務(wù)(進程的高級抽象)包含一個指針指向自己的IPC 名稱空間,在名稱空間中保存了自己的端口。此外,任務(wù)也可以獲得系統(tǒng)范圍內(nèi)的端口,例如主機端口、特權(quán)端口和其他端口。導(dǎo)出給用戶空間的端口對象實際上是對“真正”端口對象的一個句柄。

消息傳遞的實現(xiàn)

用戶態(tài)的Mach消息傳遞使用mach_msg( )函數(shù)。這個函數(shù)通過內(nèi)核的Mach 陷阱機制調(diào)用內(nèi)核函數(shù)mach_msg_trap( ) 。然后mach_msg_trap( )調(diào)用 mach_msg_overwrite_trap( ),mach_msg_overwrite_trap( ) 通過測試MACH_SEND_MSG和MACH_REV_MSG標(biāo)志位來判斷發(fā)送操作還是接收操作

發(fā)送消息

Mach 消息發(fā)送的邏輯在內(nèi)核中的兩處實現(xiàn):Mach_msg_overwrite_trap( ) 和 mach_msg_send( )。后者只用于內(nèi)核態(tài)的消息傳遞,在用戶態(tài)不可見。
兩種情形的邏輯都差不多,遵循以下的流程:

  • 調(diào)用current_space( ) 獲得當(dāng)前的IPC空間
  • 調(diào)用current_map( ) 獲得的當(dāng)前的VM空間(vm_map)
  • 對消息的大小進行正確性檢查
  • 計算要分配的消息大?。簭膕end_size參數(shù)獲得大小,然后加上硬編碼的MAX_REAILER_SIZE
  • 通過ipc_kmsg_alloc 分配消息
  • 復(fù)制消息(復(fù)制消息send_size字節(jié)的部分),然后在消息頭設(shè)置msgh_size
  • 復(fù)制消息關(guān)聯(lián)的端口權(quán)限,然后通過ipc_kmsg_copyin 將所有out-of-line 數(shù)據(jù)內(nèi)存復(fù)制到當(dāng)前的vm_map。ipc_kmsg_copyin 函數(shù)調(diào)用了ipc_kmsg_copyin_header 和 ipc_kmsg_copyin_body
  • 調(diào)用ipc_kmsg_send( )發(fā)送消息:
  • 首先,獲得msgh_remote_port 引用,并鎖定端口
  • 如果端口是一個內(nèi)核端口(即端口的ip_receiver是內(nèi)核IPC空間),那么通過ipc_kobject_server( ) 函數(shù)處理消息。這個函數(shù)會在內(nèi)核中找到相應(yīng)的函數(shù)來執(zhí)行消息(或者調(diào)用ipc_kobject_notify( )來執(zhí)行),而且一個會生成消息的應(yīng)答。
  • 不論是哪種端口:也就是說如果端口不在內(nèi)核空間中,或者從ipc_kobjct_server( ) 返回了應(yīng)答,這個函數(shù)會貫穿到傳遞消息(或應(yīng)答消息)的部分,調(diào)用ipc_mqueue_send( ),這個函數(shù)將消息直接復(fù)制到端口的ip_messgaes 隊列中并喚醒任何正在等待的線程
** 接收消息**

和消息發(fā)送的情形類似,Mach 消息接收的邏輯也是現(xiàn)在內(nèi)核中的兩個地方,和發(fā)送一樣,mach_msg_overwrite_trap( ) 從用戶態(tài)接收請求,而內(nèi)核態(tài)通過mach_msg_receive( ) 接收消息

  • 調(diào)用current_space( ) 獲得當(dāng)前的IPC空間
  • 調(diào)用current_map( ) 獲得當(dāng)前的VM控件(vm_map)
  • 不對消息的大小進行檢查。這種檢查沒有必要,因為消息在發(fā)送時已經(jīng)驗證過了
  • 通過調(diào)用ipc_mqueue_copyin( ) 獲得IPC隊列
  • 持有當(dāng)前線程的一個引用。使用當(dāng)前線程的引用可使它適應(yīng)使用Mach 的續(xù)體(continuation)模型,續(xù)體模型可以避免維護完整線程棧的必要性
  • 調(diào)用ipc_mqueue_receive( )從隊列中取出消息
  • 最后,調(diào)用mach_msg_receive_results( ) 函數(shù)。這個函數(shù)也可以從續(xù)體中調(diào)用

同步原語

消息傳遞機制只是Mach IPC架構(gòu)中的一個組件。另一個組件是同步機制(synchronization),同步機制用于判定兩個或多個并發(fā)的操作如何訪問共享資源。Mach 的同步原語如下表

對象 所有者 空可見性 等待
互斥體(lck_mtx_t) 1個 內(nèi)核態(tài) 阻塞
信號量(semaphore_t) 多個 用戶態(tài) 阻塞
自旋鎖(hw_lock_t等) 1個 內(nèi)核態(tài) 忙等
鎖集(lock_set_t) 一個 用戶態(tài) 阻塞

Mach 的鎖也是由兩個層次組合而成的:

  • 硬件相關(guān)層:依賴于硬件的特殊性質(zhì),并且通過特定的匯編指令實現(xiàn)原子性和互斥性
  • ** 硬件無關(guān)層**:通過統(tǒng)一的API包裝硬件特定的調(diào)用。這些API使得Mach 之上的層(或用戶 API)完全不用關(guān)心實現(xiàn)的細節(jié),這通常是通過一組簡單的宏實現(xiàn)的
鎖組對象

大部分Mach 同步對象都不是自己獨立存在的,而是屬于一個 lck_grp_t 對象。lck_grp_t 就是一個鏈表中的一個元素,帶有一個給定的名字,以及最多3種鎖的類型:自旋鎖、互斥鎖和讀寫鎖。鎖組還帶有統(tǒng)計信息(lck_grp_stat_t 數(shù)據(jù)結(jié)構(gòu)),用于調(diào)試和同步相關(guān)的問題。在Mach 和 BSD 中幾乎每一個子系統(tǒng)在初始化時都會創(chuàng)建一個自己使用的鎖組。

互斥體對象

互斥體是最常用的鎖對象?;コ怏w定義為lck_mtx_t,互斥體必須屬于一個鎖組。

讀寫鎖對象

互斥體有一個最大的缺點,就是一次只能有一個線程持有鎖。在很多情況下,多個線程可能對資源請求只讀的訪問,這些情況下,使用互斥鎖會阻止并發(fā)訪問。讀寫鎖(read-write lock)就是問題的解決方案。讀寫鎖是個“更智能”的互斥體,能夠區(qū)分讀訪問和寫訪問。多個讀者可以同時持有鎖,而一次只能有一個寫者可以獲得鎖。

自旋鎖對象

互斥體和信號量都是阻塞等待的對象。阻塞等待的意思是說:如果鎖對象被其他線程持有,那么請求訪問的線程就被加入到等待隊列中,因而被阻塞。阻塞一個線程就意味著放棄線程的時間片,把處理器讓給調(diào)度器認(rèn)為下一個要執(zhí)行的線程。當(dāng)鎖可用時,調(diào)度器會得到通知,然后根據(jù)自己的判斷將線程從等待隊列中取出并重新調(diào)度。然而這個方式可能會嚴(yán)重地影響性能,由于在很多情況下,鎖對象只需要持有短短幾個周期的時間,因而造成了兩次或更多次的上下文切換帶來的開銷則要大好幾個數(shù)量級。這種情況下,如果線程不是放棄處理器,而是重復(fù)地嘗試訪問鎖對象可能是更明智的選擇,這種方式稱之為“忙等(busy-wait)”,如果當(dāng)前鎖的持有者確實在幾個周期后就放棄鎖了,那么這樣就可以節(jié)省至少兩次上下文切換。當(dāng)然這個鎖要慎用,否則很可能進入一個非??膳碌乃梨i場景,導(dǎo)致整個系統(tǒng)陷入停滯狀態(tài)。

信號量對象

Mach 提供了信號量(semaphore),信號量是泛化的互斥體?;コ怏w的值只能是0和1,而信號量的值這樣的一種互斥體。取值可以達到某個正數(shù),即允許并發(fā)持有信號量的持有者的個數(shù),換句話說,互斥體可以看成是二值信號量的特殊情況。信號量可以在用戶態(tài)使用,而互斥體只能在內(nèi)核態(tài)使用。信號量本身是一個不可鎖的對象。信號量對象是一個很小的結(jié)構(gòu)體,包含指向所有者和端口的引用。此外,還保護桿一個wait_queue_t,這是一個保存正在等待這個信號量的線程的鏈表。wait_queue_t會通過硬件所的方式鎖定。信號量還有一個有意思的屬性:信號量可以轉(zhuǎn)換為端口,也可以由端口轉(zhuǎn)換而來。

鎖集對象

任務(wù)可以在用戶態(tài)使用鎖集。鎖集就是鎖(實際上就是互斥體)的數(shù)組。通過給定的鎖ID 可以訪問鎖。鎖也可以傳遞給其他線程。交出一個鎖會阻塞交出鎖的線程,并喚醒接受鎖的線程。鎖集實際上是對內(nèi)核互斥體lck_mtx_t的封裝,如下圖所示:

通過互斥體實現(xiàn)鎖集的過程.png

鎖集的有趣之處在于允許鎖的傳遞。鎖的傳遞指是將鎖從一個任務(wù)傳遞給另一個任務(wù)的過程。Mach 在調(diào)度中也使用了傳遞的概念,允許一個線程放棄處理器但是指定哪一個線程接替允許。

機器原語

Mach 通過一些所謂的“機器原語”對運行的機器進行抽象,機器原語處理的對象包括主機、時鐘、處理器以及處理器集。

主機對象

Mach 最基礎(chǔ)的對象是“主機(host)”,也就是表示機器本身的對象。主機對象是一個簡單的數(shù)據(jù)結(jié)構(gòu)。主機只不過是一組“特殊端口”的集合(用于向主機發(fā)送各種消息),以及一組異常處理程序的集合。主機定于了一個鎖組用于保護異常處理的并發(fā)訪問。
主機的數(shù)據(jù)結(jié)構(gòu)主要有三個基本功能:

  • 提供機器信息:Mach 提供了一組異常豐富的API調(diào)用用于查詢機器信息,所有這些調(diào)用都要求獲得主機端口才能工作。
  • 提供子系統(tǒng)的訪問:通過主機抽象,應(yīng)用程序可以請求訪問子系統(tǒng)使用的任何“特殊”端口。此外,還可以獲得所有其他機器抽象(例如:processor 和 processor_set)的訪問權(quán)。
  • 提供默認(rèn)的異常處理:異常從線程基本提升到進程(任務(wù))基本,如果沒有被處理的話。則進一步提升到主機級別做通用的處理。
時鐘對象

Mach 內(nèi)核提供了一個簡單的“時鐘(clock)”對象抽象。這個對象用于計時和鬧鐘。時鐘是一個帶有兩個端口的對象:一個用于“服務(wù)類”的函數(shù)(例如報時或鬧鈴),另一個用于“控制類”的函數(shù),例如設(shè)置一天中的時間。

處理器對象

處理器(processor)對象表示機器上的一個邏輯CPU 或 CPU 核心。如今多核架構(gòu)已是默認(rèn)架構(gòu),多核架構(gòu)中的每一個核心都可以看出一個CPU,處理器被分配給處理器集,處理器集是一個或多個處理器的邏輯分組。處理器是CPU的簡單抽象,被Mach 用于一些基本的操作,例如啟動和關(guān)閉一個CPU,以及向CPU分發(fā)要執(zhí)行的線程。

處理器集對象

一個或多個processor_t 對象可以分組為處理器集(processor set),或稱為pset(這是processor對象中的processor_set 成員),處理器集是將處理器綁定在一起的邏輯分組,Mach 可以以處理器集作為相關(guān)處理器的容器,從而能夠高效地擴展到SMP架構(gòu)。
pset 中的處理器通過兩個隊列進行維護:一個是active_queue,保存當(dāng)前正在執(zhí)行的處理器,另一個是idle_queue,用于保存當(dāng)前空閑的處理器(即正在執(zhí)行idle_thread的處理器)。處理器集還有一個全局的run_queue(pset_runq),這個隊列保存了在這個集合中的處理器上執(zhí)行的線程。和其他所有對象一樣,處理器集也暴露一些端口:pset_self(用于對處理器集進行操作) 和 pset_name_self(用于獲得處理器集的消息)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容