細讀《深入理解 Android 內核設計思想》(一)進程間通信與同步機制

對冗余挑揀重點,對重點深入補充,輸出結構清晰的精簡版

1.進程間通信的經典實現
共享內存、管道
UNIX Domain Socket
Remote Procedure Calls
2.同步機制的經典實現
信號量、Mutex、管程、Linux Futex
3.Android 中的進程間同步機制
Mutex、Condition、Autolock
Mutex+Autolock+Condition 示例
4.最后

進程間通信的經典實現

進程間通信(Inter-process communication,IPC)指運行在不同進程中的若干線程間的數據交換,可發生在同一臺機器上,也可通過網絡跨機器實現,以下幾種因高效穩定的優點幾乎被應用在所有操作系統中,分別是共享內存、管道、UNIX Domain Socket 和 RPC 。

共享內存

共享內存是一種常用的進程間通信機制,不同進程可以直接共享訪問同一塊內存區域,避免了數據拷貝,速度較快。實現步驟:

  1. 創建內存共享區
    Linux 通過 shmget 方法創建與特定 key 關聯的共享內存塊:
//返回共享內存塊的唯一 Id 標識
int shmget(key_t key, size_t size, int shmflg);               
  1. 映射內存共享區
    Linux 通過 shmat 方法將某內存塊與當前進程某內存地址映射
//成功返回指向共享存儲段的指針 
void *shmat(int shm_id, const void *shm_addr, int shmflg);        
  1. 訪問內存共享區
    其他進程要訪問一個已存在的內存共享區的話,可以通過 key 調用 shmget 獲取到共享內存塊 Id,然后調用 shmat 方法映射
  2. 進程間通信
    當兩個進程都實現對同一塊內存共享區做映射后,就可以利用此內存共享區進行數據交換,但要自己實現同步機制
  3. 撤銷內存映射
    進程間通信結束后,各個進程需要撤銷之前的映射,Linux 可以調用 shmdt 方法撤銷映射:
//成功則返回 0,否則出錯
int shmdt(const void *shmaddr);
  1. 刪除內存共享區
    最后需要刪除內存共享區,以便回收內存,Linux 可以調用 shctl 進行刪除:
//成功則返回 0,否則出錯,刪除操作 cmd 需傳 IPC_RMID
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

shmget 方法名言簡意賅,share memory get !其中 get 還有一層含義,為什么不叫 create 呢?之前如果創建過某一 key 的共享內存塊,再次調用便直接返回該內存塊,不會發生創建操作了。

管道

管道(Pipe)是操作系統中常見的一種進程間通信方式,一根管道有"讀取"和"寫入"兩端,讀、寫操作和普通文件操作類似,并且是單向的。管道有容量限制,當寫滿時,寫操作會被阻塞;為空時讀操作會被阻塞。

Linux 通過 pipe 方法打開一個管道:

//pipe_fd[0] 代表讀端,pipe_fd[1] 代表寫端,
int pipe(int pipe_fd[2], int flags);

以上方式只能用于父子進程,因為只有一個進程中定義的 pipe_fd 文件描述符只有通過 fork 方式才能傳給另一個進程繼承獲取到,也正是因為這個限制,Named Pipe 得以發展,改變了前者匿名管道的方式,可以在沒有任何關系的兩個進程間使用。

UNIX Domain Socket

UNIX Domain Socket(UDS)是專門針對單機內的進程間通信,也稱 IPC Socket,與 Network Socket 使用方法基本一致,但實現原理區別很大:

  • Network Socket 基于 TCP/IP 協議,通過 IP 地址或端口號進行跨進程通信
  • UDS 基于本機 socket 文件,不需要經過網絡協議棧,不需要打包拆包、計算校驗等

Android 中使用最多的 IPC 是 Binder,其次就是 UDS。

Remote Procedure Calls

RPC 即遠程過程調用(Remote Procedure Call),RPC 是指計算機 A 上的進程,調用另外一臺計算機 B 上的進程,其中 A 上的調用進程被掛起,而 B 上的被調用進程開始執行,當值返回給 A 時,A 進程繼續執行。調用方可以通過使用參數將信息傳送給被調用方,而后可以通過傳回的結果得到信息。

Java RMI 就是一種 RPC 框架,指的是遠程方法調用 (Remote Method Invocation)。它能夠讓一個 Java 虛擬機上的對象調用另一個 Java 虛擬機中的對象的方法。

RPC 可以理解為一種編程模型,就像 IPC 一樣,比如我們常說 Android AIDL 是一種 IPC 實現方式,也可以稱為一種 RPC 方式。

同步機制的經典實現

信號量

信號量與 PV 原語操作是一種廣泛使用的實現進程/線程互斥與同步的有效方法,Semaphore S 信號量用于指示共享資源的可用數量。
P 操作:

  1. S = S - 1
  2. 然后判斷若 S 大于等于 0,代表共享資源允許訪問,進程繼續執行
  3. 若 S 小于 0,代表共享資源被占用,需等待別人主動釋放資源,該進程阻塞放入等待該信號量的隊列中,等待被喚醒

V 操作:

  1. S = S + 1
  2. 然后判斷若 S 大于 0,代表沒有正在等待訪問該資源的進程,無需處理
  3. 若 S 小于等于 0,從該信號的等待隊列中喚醒一個進程

Java 中的信號量的實現類為 Semaphore,P、V 操作分別對應 acquire、release 方法。

Mutex

Mutex 即互斥鎖,可以和信號量對比來理解,信號量可以使資源同時被多個線程訪問,而互斥鎖同時只能被一個線程訪問,也就是說,互斥鎖相當于一個只允許取值 0 或 1 的信號量。

Java 中 ReentrantLock 就是互斥鎖的一種實現。

管程

采用 Semaphore 機制的程序中 P、V 操作大量分散在程序中,代碼易讀性差,不易管理,容易發生死鎖,所以引入了管程 Monitor。

管程把分散在各進程中的臨界區集中起來進行管理,防止進程有意或無意的違法同步操作,便于用高級語言來書寫程序,也便于程序正確性驗證。

管程封裝了同步操作,對進程隱蔽了同步細節,簡化了同步功能的調用界面。用戶編寫并發程序如同編寫順序(串行)程序。

Java 中 synchronized 同步代碼塊就是 Monitor 的一種實現。

Linux Futex

Futex 全稱 Fast Userspace muTexes,直譯為快速用戶空間互斥體,那他比普通的 Mutex 快在哪里呢?

Semaphore 等傳統同步機制需要從用戶態進入到內核態,通過一個提供了共享狀態信息和原子操作的內核對象來完成同步。但大多數場景同步是無競爭的,不需要進入互斥區等待就可以直接獲取到鎖,但依然進行了內核態的切換操作,這造成了大量的性能開銷。

Futex 通過 mmap 讓進程間共享一段內存,當進程嘗試進入互斥區或退出互斥區的時候,先查看共享內存中的 Futex 變量,如果沒有競爭發生,則只修改 Futex 變量而不執行系統調用切換內核態。

Futex 的 Fast 就體現在對于大多數不存在競爭的情況,可以在用戶態就完成鎖的獲取,而不需要進入內核態,從而提高了效率。

如果說 Semaphore 等傳統同步機制是一種內核態同步機制,那 Futex 就是一種用戶態和內核態混合的同步機制。

Futex 在 Android 中的一個重要應用場景是 ART 虛擬機,如果 Android 版本開啟了 ART_USE_FUTEXES 宏,那 ART 虛擬機中的同步機制就會以 Futex 為基石來實現,省略后的關鍵代碼如下:

// art/runtime/base/mutex.cc
void Mutex::ExclusiveLock(Thread* self){
    #if ART_USE_FUTEXES
        //若開啟 Futex 宏就通過 Futex 實現互斥加鎖
        futex(...) 
    #else
        //否則通過傳統 pthread 實現
        CHECK_MUTEX_CALL(pthread_mutex_lock,(&mutex_));
}

源碼見 http://androidxref.com/7.0.0_r1/xref/art/runtime/base/mutex.cc

Android 中的進程間同步機制

了解了操作系統經典的同步機制后,再來看 Android 中是怎么實現的。

進程間同步 Mutex

Mutex 實現類源碼很短,見 http://androidxref.com/7.0.0_r1/xref/system/core/include/utils/Mutex.h

注意這里說的 Mutex 和上面的 mutex.cc 是兩個東西,mutex.cc 是 ART 中的實現類,支持 Futex 方式; 而 Mutex.h 只是對 pthread 的 API 進行了簡單封裝,函數聲明和實現都在 Mutex.h 一個文件中。

源碼中可以看到一個枚舉類型定義:

class Mutex {
public:
    enum {
        PRIVATE = 0,
        SHARED = 1
    };

其中 PRIVATE 代表進程內同步,SHARED 代表進程間同步。Mutex 相比 Semaphore 較簡單,只有 0 和 1 兩種狀態,關鍵方法為:

inline status_t Mutex::lock() {//獲取資源鎖,可能阻塞等待
    return -pthread_mutex_lock(&mMutex);
}
inline void Mutex::unlock() {//釋放資源鎖
    pthread_mutex_unlock(&mMutex);
}
inline status_t Mutex::tryLock() {//獲取資源鎖,不論成功與否都立即返回
    return -pthread_mutex_trylock(&mMutex);
}

當要訪問臨界資源時,需先通過 lock() 獲得資源鎖,如果資源可用會此函數會立即返回,否則阻塞等待,直到其他進程(線程)調用 unlock() 釋放了資源鎖從而被喚醒。

tryLock() 函數存在有什么意義呢?它在資源被占用的情況下,不會像 lock() 一樣進入等待,而是立即返回,所以可以用來試探性查詢資源鎖是否被占用。

加解鎖的自動化操作 Autolock

Autolock 為 Mutex.h 中的一個嵌套類,實現如下:

// Manages the mutex automatically. It'll be locked when Autolock is
// constructed and released when Autolock goes out of scope.
class Autolock {
public:
    inline Autolock(Mutex& mutex) : mLock(mutex)  { mLock.lock(); }
    inline Autolock(Mutex* mutex) : mLock(*mutex) { mLock.lock(); }
    inline ~Autolock() { mLock.unlock(); }
private:
    Mutex& mLock;
};

如注釋所示,Autolock 會在構造時主動去獲取鎖,在析構時會自動釋放掉鎖,也就是說,在生命周期結束時會自動把資源鎖釋放掉。

這就可以在一個方法開始時為某 Mutex 構造一個 Autolock,當方法執行完后此鎖會自動釋放,無需再主動調用 unlock,這讓 lock/unlock 的配套使用更加簡便,不易出錯,

條件判斷 Condition

條件判斷的核心思想是判斷某 "條件" 是否滿足,滿足的話馬上返回,否則阻塞等待,直到條件滿足時被喚醒。

你可能會疑問,Mutex 不就可以實現嗎,干嘛又來一個 Condition,它有什么特別之處?

Mutex 確實可以實現基于條件判斷的同步,假如條件是 a 為 0,實現代碼會是這樣:

while(1){
  acquire_mutex_lock(a); //獲取 a 的互斥鎖
  if(a==0){
    release_mutex_lock(a); //釋放鎖
    break; //條件滿足,退出死循環
  }else{
    release_mutex_lock(a); //釋放鎖
    sleep();//休眠一段時間后繼續循環
  }
}

什么時候滿足 a==0 是未知的,可能是很久之后,但上面方式無限循環去判斷條件,極大浪費 CPU。

而條件判斷不需要死循環,可以在滿足條件時才去通知喚醒等待者。

Condition 源碼見 http://androidxref.com/7.0.0_r1/xref/system/core/include/utils/Condition.h ,它和 Mutex 一樣也有 PRIVATE、SHARED 類型,PRIVATE 代表進程內同步,SHARED 為進程間同步。關鍵方法為:

//在某個條件上等待
status_t wait(Mutex& mutex)
//在某個條件上等待,增加超時機制
status_t waitRelative(Mutex& mutex, nsecs_t reltime)
//條件滿足時通知相應等待者
void signal()
//條件滿足時通知所有等待者
void broadcast()

Mutex+Autolock+Condition 示例

書中通過 Barrier 呈現 Condition 使用示例,還有一個我們更為熟知的 LinkedBlockingQueue 也很適合,源碼見 http://androidxref.com/7.0.0_r1/xref/frameworks/av/media/libstagefright/webm/LinkedBlockingQueue.h

class LinkedBlockingQueue {
    List<T> mList;
    Mutex mLock;
    Condition mContentAvailableCondition;

    T front(bool remove) {
        Mutex::Autolock autolock(mLock);
        while (mList.empty()) {
            mContentAvailableCondition.wait(mLock);
        }
        T e = *(mList.begin());
        if (remove) {
            mList.erase(mList.begin());
        }
        return e;
    }
    //省略...

    void push(T e) {
        Mutex::Autolock autolock(mLock);
        mList.push_back(e);
        mContentAvailableCondition.signal();
    }
}

調用 front 方法出隊元素時,首先獲取 mLock 鎖,然后判斷若列表為空就調用 wait 方法進入等待狀態,待 push 方法入隊元素后通過 signal 方法喚醒。

front 方法占有了 mLock 鎖,push 方法不應該阻塞在第一行代碼無法往下執行嗎?

很簡單,wait 方法中釋放了 mLock 鎖,見 pthread_cond.cpp:http://androidxref.com/7.0.0_r1/xref/bionic/libc/bionic/pthread_cond.cpp#173

可以不依賴 Mutex 僅通過 Condition 的 wait/signal 實現嗎?

不行,因為對 mList 的訪問需要加互斥鎖,否則可能出現 signal 無效的情況。比如 A 進程調用 front ,判斷 mList 為空,即將執行 wait 方法時,B 進程調用 push 方法并執行完,那么 A 進程將得不到喚醒,盡管此隊列中有元素。

最后

書中說到:不論什么樣的操作系統,其技術本質都類似,而更多的是把這些核心的理論應用到符合自己需求的場景中。

不知道在講這句話時,作者腦中一閃而過的,是怎樣龐大而深厚的技術棧。

鏈接:細讀《深入理解 Android 內核設計思想》系列

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內容