對冗余挑揀重點,對重點深入補充,輸出結構清晰的精簡版
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 。
共享內存
共享內存是一種常用的進程間通信機制,不同進程可以直接共享訪問同一塊內存區域,避免了數據拷貝,速度較快。實現步驟:
- 創建內存共享區
Linux 通過 shmget 方法創建與特定 key 關聯的共享內存塊:
//返回共享內存塊的唯一 Id 標識
int shmget(key_t key, size_t size, int shmflg);
- 映射內存共享區
Linux 通過 shmat 方法將某內存塊與當前進程某內存地址映射
//成功返回指向共享存儲段的指針
void *shmat(int shm_id, const void *shm_addr, int shmflg);
- 訪問內存共享區
其他進程要訪問一個已存在的內存共享區的話,可以通過 key 調用 shmget 獲取到共享內存塊 Id,然后調用 shmat 方法映射 - 進程間通信
當兩個進程都實現對同一塊內存共享區做映射后,就可以利用此內存共享區進行數據交換,但要自己實現同步機制 - 撤銷內存映射
進程間通信結束后,各個進程需要撤銷之前的映射,Linux 可以調用 shmdt 方法撤銷映射:
//成功則返回 0,否則出錯
int shmdt(const void *shmaddr);
- 刪除內存共享區
最后需要刪除內存共享區,以便回收內存,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 操作:
- S = S - 1
- 然后判斷若 S 大于等于 0,代表共享資源允許訪問,進程繼續執行
- 若 S 小于 0,代表共享資源被占用,需等待別人主動釋放資源,該進程阻塞放入等待該信號量的隊列中,等待被喚醒
V 操作:
- S = S + 1
- 然后判斷若 S 大于 0,代表沒有正在等待訪問該資源的進程,無需處理
- 若 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 進程將得不到喚醒,盡管此隊列中有元素。
最后
書中說到:不論什么樣的操作系統,其技術本質都類似,而更多的是把這些核心的理論應用到符合自己需求的場景中。
不知道在講這句話時,作者腦中一閃而過的,是怎樣龐大而深厚的技術棧。