Android跨進程通信IPC之3——Bionic

移步系列Android跨進程通信IPC系列
Bionic庫是Android的基礎庫之一,也是連接Android系統和Linux系統內核的橋梁,Bionic中包含了很多基本的功能模塊,這些功能模塊基本上都是源于Linux,但是就像青出于藍而勝于藍,它和Linux還是有一些不一樣的的地方。

1 谷歌為什么使用Bionic庫

谷歌使用Bionic庫主要因為以下三點

  • 1、谷歌沒有使用Linux的GUN Libc,很大一部分原因是因為GNU Libc的授權方式是GPL 授權協議有限制,因為一旦軟件中使用了GPL的授權協議,該系統所有代碼必須開元。
  • 2、谷歌在BSD的C庫上的基礎上加入了一些Linux特性從而生成了Bionic。Bionic名字的來源就是BSD和Linux的混合。而且不受限制的開源方式,所以在現代的商業公司中比較受歡迎。
  • 3、還有就是因為性能的原因,因為Bionic的核心設計思想就是"簡單",所以Bionic中去掉了很多高級功能。這樣Bionic庫僅為200K左右,是GNU版本體積的一半,這意味著更高的效率和低內存的使用,同時配合經過優化的Java VM Dalvik才可以保證高的性能。

2 Bionic庫簡介

  • Bionic 音標為 bī??nik,翻譯為"仿生"
  • Bionic包含了系統中最基本的lib庫,包括libc,libm,libdl,libstd++,libthread_db,以及Android特有的鏈接器linker。

3 Bionic庫的特性

3.1 架構

Bionic 當前支持ARM、x86和MIPS執行集,理論上可以支持更多,但是需要做些工作,ARM相關的代碼在目錄arch-arm中,x86相關代碼在arch-x86中,mips相關的代碼在arch-mips中。

3.2 Linux核心頭文件

Bionic自帶一套經過清理的Linxu內核頭文件,允許用戶控件代碼使用內核特有的聲明(如iotcls,常量等)這些頭文件位于目錄:
bionic/libc/kernel/common
bionic/libc/kernel/arch-arm
bionic/libc/kernel/arch-x86
bionic/libc/kernel/arch-mips

3.3 DNS解析器

雖然Bionic 使用NetBSD-derived解析庫,但是它也做了一些修改。

  • 1、不實現name-server-switch特性
  • 2、讀取/system/etc/resolv.conf而不是/etc/resolv.config
  • 3、從系統屬性中讀取服務器地址列表,代碼中會查找'net.dns1','net.dns2',等屬性。每個屬性都應該包含一個DNS服務器的IP地址。這些屬性能被Android系統的其它進程修改設置。在實現上,也支持進程單獨的DNS服務器列表,使用屬性'net.dns1.<pid>'、'net.dns2.<pid>'等,這里<pid> 表示當前進程的ID號。
  • 4、在執行查詢時,使用一個合適的隨機ID(而不是每次+1),以提升安全性。
  • 5、在執行查詢時,給本地客戶socket綁定一個隨機端口號,以提高安全性。
    6、刪除了一些源代碼,這些源代碼會造成了很多線程安全的問題

3.4 二進制兼容性

由于Bionic不與GNU C庫、ucLibc,或者任何已知的Linux C相兼容。所以意味著不要期望使用GNU C庫頭文件編譯出來的模塊能夠正常地動態鏈接到Bionic

3.5 Android特性

Bionict提供了少部分Android特有的功能

3.5.1 訪問系統特性

Android 提供了一個簡單的"共享鍵/值 對" 空間給系統的中的所有進程,用來存儲一定數量的"屬性"。每個屬性由一個限制長度的字符串"鍵"和一個限制長度的字符串"值"組成。
頭文件<sys/system_properties.h>中定義了讀系統屬性的函數,也定義了鍵/值對的最大長度。

3.5.2 Android用戶/組管理

在Android中沒有etc/password和etc/groups 文件。Android使用擴展的Linux用戶/組管理特性,以確保進程根據權限來對不同的文件系統目錄進行訪問。
Android的策略是:

  • 1、每個已經安裝的的應用程序都有自己的用戶ID和組ID。ID從10000(一萬)開始,小于10000(一萬)的ID留給系統的守護進程。
  • 2、tpwnam()能識別一些硬編碼的子進程名(如"radio"),能將他們翻譯為用戶id值,它也能識別"app_1234",這樣的組合名字,知道將后面的1234和10000(一萬)相加,得到的ID值為11234.getgrname()也類似。
  • 3、getservent() Android中沒有/etc/service,C庫在執行文件中嵌入只讀的服務列表作為代替,這個列表被需要它的函數所解析。所見文件bionic/libc/netbsd/net/getservent.c和bionic/libc/netbsd/net/service.h。
    這個內部定義的服務列表,未來可能有變化,這個功能是遺留的,實際很少使用。getservent()返回的是本地數據,getservbyport()和getservbyname()也按照同樣的方式實現。
  • 4、getprotoent() 在Android中沒有/etc/protocel,Bionic目前沒有實現getprotocent()和相關函數。如果增加的話,很可能會以getervent()相同的方式。

4 Bionic庫的模塊簡介

Bionic目錄下一共有5個庫和一個linker程序
5個庫分別是:

  • 1、libc
  • 2、libm
  • 3、libdl
  • 4、libstd++
  • 5、libthread_db

4.1 Libc庫

Libc是C語言最基礎的庫文件,它提供了所有系統的基本功能,這些功能主要是對系統調用的封裝,是Libc是應用和Linux內核交流的橋梁,主要功能如下:

  • 進程管理:包括進程的創建、調度策略和優先級的調整
  • 線程管理:包括線程的創建和銷毀,線程的同步/互斥等
  • 內存管理:包括內存分配和釋放等
  • 時間管理:包括獲取和保存系統時間、獲取當前系統運行時長等
  • 時區管理:包括時區的設置和調整等
  • 定時器管理:提供系統的定時服務
  • 文件系統管理:提供文件系統的掛載和移除功能
  • 文件管理:包括文件和目錄的創建增刪改
  • 網絡套接字:創建和監聽socket,發送和接受
  • DNS解析:幫助解析網絡地址
  • 信號:用于進程間通信
  • 環境變量:設置和獲取系統的環境變量
  • Android Log:提供和Android Log驅動進行交互的功能
  • Android 屬性:管理一個共享區域來設置和讀取Android的屬性
  • 標準輸入/輸出:提供格式化的輸入/輸出
  • 字符串:提供字符串的移動、復制和比較等功能
  • 寬字符:提供對寬字符的支持。

4.2 Libm庫

Libm 是數學函數庫,提供了常見的數學函數和浮點運算功能,但是Android浮點運算時通過軟件實現的,運行速度慢,不建議頻繁使用。

4.3 libdl庫

libdl庫原本是用于動態庫的裝載。很多函數實現都是空殼,應用進程使用的一些函數,實際上是在linker模塊中實現。

4.4 libstd++

libstd++ 是標準的C++的功能庫,但是,Android的實現是非常簡單的,只是new,delete等少數幾個操作符的實現。

4.5 libthread_db庫

libthread_db 用來支持對多線程的中動態庫的調試。

4.6 Linker模塊

Linux系統上其實有兩種并不完全相同的可執行文件

  • 一種是靜態鏈接的可執行程序。靜態可執行程序包含了運行需要的所有函數,可以不依賴任何外部庫來運行。
  • 另一種是動態鏈接的可執行程序。動態鏈接的可執行程序因為沒有包含所需的庫文件,因此相對于要小很多。
    靜態可執行程序用在一些特殊場合,例如,系統初始化時,這時整個系統還沒有準備好,動態鏈接的程序還無法使用。系統的啟動程序Init就是一個靜態鏈接的例子。在Android中,會給程序自動加上兩個".o"文件,分別是"crtbegin_static.c"和"certtend_android.o",這兩個".o"文件對應的源文件位于bionic/libc/arch-common/bionic目錄下,文件分別是crtbegin.c和certtend.S。_start()函數就位于cerbegin.c中。
    在動態鏈接時,execuve()函數會分析可執行文件的文件頭來尋找鏈接器,Linux文件就是ld.so,而Android則是Linker。execuve()函數將會將Linker載入到可執行文件的空間,然后執行Linker的_start()函數。Linker完成動態庫的裝載和符號重定位后再去運行真正的可執行文件的代碼。

5 Bionic庫的內存管理函數

5.1 內存管理函數

  • 對于32位的操作系統,能使用的最大地址空間是4GB,其中地址空間03GB分配給用戶進程使用,地址空間3GB4GB由內核使用,但是用戶進程并不是在啟動時就獲取了所有的0~3GB地址空間的訪問權利,而是需要事先向內核申請對模塊地址空間的讀寫權利。
  • 而且申請的只是地址空間而已,此時并沒有分配真是的物理地址。
  • 只有當進程訪問某個地址時,如果該地址對應的物理頁面不存在,則由內核產生缺頁中斷,在中斷中才會分配物理內存并建立頁表。
  • 如果用戶進程不需要某塊空間了,可以通過內核釋放掉它們,對應的物理內存也釋放掉。
  • 但是由于缺頁中斷會導致運行緩慢,如果頻繁的地由內核來分配和釋放內存將會降低整個體統的性能,因此,一般操作系統都會在用戶進程中提供地址空間的分配和回收機制。
  • 用戶進程中的內存管理會預先向內核申請一塊打的地址空間,稱為。當用戶進程需要分配內存時,由內存管理器從堆中尋找一塊空閑的內存分配給用戶進程使用。
  • 當用戶進程釋放某塊內存時,內存管理器并不會立刻將它們交給內核釋放,而是放入空閑列表中,留待下次分配使用。
  • 內存管理器會動態的調整堆的大小,如果堆的空間使用完了,內存管理器會向堆內存申請更多的地址空間,如果堆中空閑太多,內存管理器也會將一部分空間返給內核。

5.2 Bionic的內存管理器——dlmalloc

  • dlmalloc是一個十分流行的內存分配器。

  • dlmalloc位于bionic/libc/upstream-dlmalloc下,只有一個C文件malloc.c。

  • dlmalloc內部是以鏈表的形式將"堆"的空閑空間根據尺寸組織在一起。分配內存時通過這些鏈表能快速地找到合適大小的空閑內存。如果不能找到滿足要求的空閑內存,dlmalloc會使用系統調用來擴大堆空間。

  • dlmalloc內存塊被稱為"trunk"。每塊大小要求按地址對齊(默認8個字節),因此,trunk塊的大小必須為8的倍數。

  • dlmalloc用3種不同的的鏈表結構來組織不同大小的空閑內存塊。小于256字節的塊使用malloc_chunk結構,按照大小組織在一起。由于尺寸小于的塊一共有256/8=32,所以一共使用了32個malloc_chunk結構的環形鏈表來組織小于256的塊。大小大于256字節的塊由結構malloc_tree_chunk組成鏈表管理,這些塊根據大小組成二叉樹。而更大的尺寸則由系統通過mmap的方式單獨分配一塊空間,并通過malloc_segment組成的鏈表進行管理。

  • 當dlmalloc分配內存時,會通過查找這些鏈表來快速找到一塊和要求的尺寸大小最匹配的空閑內存塊(這樣做事為了盡量避免內存碎片)。如果沒有合適大小的塊,則將一塊大的分成兩塊,一塊分配出去,另一塊根據大小再加入對應的空閑鏈表中。

  • 當dlmalloc釋放內存時,會將相鄰的空閑塊合并成一個大塊來減少內存碎片。如果空閑塊過多,超過了dlmaloc內存的閥值,dlmalloc就開始向系統返回內存。

  • dlmalloc除了能管理進程的"堆"空間,還能提供私有堆管理,就是在堆外單獨分配一塊地址空間,由dlmalloc按照同樣的方式進行管理。dlmalloc中用來管理進程的"堆"空間的函數,都帶有"dl"前綴,如"dlmalloc","dlfree"等,而私有堆的管理函數則帶有前綴"msspace_",如"msspace_malloc"

  • Dalvk虛擬機中使用了dlmalloc進行私有堆管理。

6 線程

Bionic中的線程管理函數和通用的Linux版本的實現有很多差異,Android根據自己的需要做了很多裁剪工作。

6.1 Bionic線程函數的特性

6.1.1 pthread的實現基于Futext,同時盡量使用簡單的代碼來實現通用操作,特征如下

  • pthread_mutex_t,pthread_cond_t類型定義只有4字節。
  • 支持normal,recursive and error-check 互斥量。考慮到通常大多數的時候都使用normal,對normal分支下代碼流程做了很細致的優化
  • 目前沒有支持讀寫鎖,互斥量的優先級和其他高級特征。在Android還不需要這些特征,但是在未來可能會添加進來。

6.1.2 Bionic不支持pthread_cancel(),因為加入它會使得C庫文件明顯變大,不太值得,同時有以下幾點考慮

  • 要正確實現pthread_cancel(),必須在C庫的很多地方插入對終止線程的檢測。
  • 一個好的實現,必須清理資源,例如釋放內存,解鎖互斥量,如果終止恰好發生在復雜的函數里面(比如gthosbyname()),這會使許多函數正常執行也變慢。
  • pthread_cancel()不能終止所有線程。比如無窮循環中的線程。
  • pthread_cancel()本身也有缺點,不太容易移植。
  • Bionic中實現了pthread_cleanup_push()和pthread_cleanup_pop()函數,在線程通過調用pthread_exit()退出或者從它的主函數中返回到時候,它們可以做些清理工作。

6.1.3 不要在pthread_once()的回調函數中調用fork(),這么做會導致下次調用pthread_once()的時候死鎖。而且不能在回調函數中拋出一個C++的異常。

6.1.4 不能使用_thread關鍵詞來定義線程本地存儲區。

6.2 創建線程和線程的屬性

6.2.1 創建線程

函數pthread_create()用來創建線程,原型是:

int  pthread_create((pthread_t  *thread,  pthread_attr_t  *attr,  void  *(*start_routine)(void  *),  void  *arg)

其中,pthread_t在android中等同于long

  • 參數thread是一個指針,pthread_create函數成功后,會將代表線程的值寫入其指向的變量。
  • 參數 args 一般情況下為NULL,表示使用缺省屬性。
  • 參數start_routine是線程的執行函數
  • 參數arg是傳入線程執行函數的參數

若線程創建成功,則返回0,若線程創建失敗,則返回出錯編號。
PS:要注意的是,pthread_create調用成功后線程已經創建完成,但是不會立刻發生線程切換。除非調用線程主動放棄執行,否則只能等待線程調度。

6.2.2 線程的屬性

結構 pthread_atrr_t用來設置線程的一些屬性,定義如下:

typedef struct
{
    uint32_t        flags;                
    void *    stack_base;              //指定棧的起始地址
    size_t    stack_size;               //指定棧的大小
    size_t    guard_size;  
    int32_t   sched_policy;           //線程的調度方式
    int32_t   sched_priority;          //線程的優先級
}

使用屬性時要先初始化,函數原型是:

int  pthread_attr_init(pthread_attr_t*  attr)

通過pthread_attr_init()函數設置的缺省屬性值如下:

int pthread_attr_init(pthread_attr_t* attr){
     attr->flag=0;
     attr->stack_base=null;
     attr->stack_szie=DEFAULT_THREAD_STACK_SIZE;  //缺省棧的尺寸是1MB
     attr0->quard_size=PAGE_SIZE;//大小是4096
     attr0->sched_policy=SCHED_NORMAL;//普通調度方式
     attr0->sched_priority=0;//中等優先級
     return 0;
}

下面介紹每項屬性的含義

  • 1、flag 用來表示線程的分離狀態
    Linux線程有兩種狀態:分離(detch)狀態和非分離(joinable)狀態,如果線程是非分離狀態(joinable)狀態,當線程函數退出時或者調用pthread_exit()時都不會釋放線程所占用的系統資源。只有當調用了pthread_join()之后這些資源才會釋放。如果是分離(detach)狀態的線程,這些資源在線程函數退出時調用pthread_exit()時會自動釋放
  • 2、stack_base: 線程棧的基地址
  • 3、stack_size: 線程棧的大小?;刂泛蜅5拇笮?。
  • 4、guard_size: 線程的棧溢出保護區大小。
  • 5、sched_policy:線程的調度方式。
    線程一共有3中調度方式:SCHED_NORMAL,SCHED_FIFO,SCHED_RR。其中SCHED_NORMAL代表分時調度策略,SCHED_FIFO代表實時調度策略,先到先服務,一旦占用CPU則一直運行,一直運行到有更高優先級的任務到達,或者自己放棄。SCHED_RR代表實時調度策略:時間片輪轉,當前進程時間片用完,系統將重新分配時間片,并置于就緒隊尾。
  • 6、sched_priority:線程的優先級。

Bionic雖然也實現了pthread_attr_setscope()函數,但是只支持PTHREAD_SCOP_SYSTEM屬性,也就意味著Android線程將在全系統的范圍內競爭CPU資源。

6.3 退出線程的方法

6.3.1 調用pthread_exit函數退出

一般情況下,線程運行函數結束時線程才退出。但是如果需要,也可以在線程運行函數中調用pthread_exit()函數來主動退出線程運行。函數原型如下:

 void pthread_exit( void * retval) ;

其中參數retval用來設置返回值

6.3.2 設備布爾的全局變量

但是如果希望在其它線程中結束某個線程?前面介紹了Android不支持pthread_cancel()函數,因此,不能在Android中使用這個函數來結束線程。通俗的方法是,如果線程在一個循環中不停的運行,可以在每次循環中檢查一個初始值為false的全局變量,一旦這個變量的值為ture,則主動退出,這樣其它線程就可以銅鼓改變這個全局變量的值來控制線程的退出,示例如下:

bool g_force_exit =false;
void * thread_func(void *){
         for(;;){
             if(g_force_exit){
                    break;
             }
             .....
         }
         return NULL;
}
int main(){
     .....
     q_force_exit=true;       //青坡線程退出
}

這種方法實現起來簡單可靠,在編程中經常使用。但它的缺點是:如果線程處于掛起等待狀態,這種方法就不適用了。
另外一種方式是使用pthread_kill()函數。pthread_kill()函數的作用不是"殺死"一個線程,而是給線程發送信號。函數如下:

  int pthread_kill(pthread tid,int sig);

即使線程處于掛起狀態,也可以使用pthead_kill()函數來給線程發送消息并使得線程執行處理函數,使用pthread_kill()函數的問題是:線程如果在信號處理函數中退出,不方便釋放在線程的運行函數中分配的資源。

6.3.3 通過管道

更復雜的方法是:創建一個管道,在線程運行函數中對管道"讀端"用select()或epoll()進行監聽,沒有數據則掛起線程,通過管道的"寫端"寫入數據,就能喚起線程,從而釋放資源,主動退出。

6.4 線程的本地存儲TLS

線程本地存儲(TLS)用來保存、傳遞和線程有關的數據。例如在前面說道的使用pthread_kill()函數關閉線程的例子中,需要釋放的資源可以使用TLS傳遞給信號處理函數。

6.4.1 TLS介紹

  • TLS在線程實例中是全局可見的,對某個線程實例而言TLS是這個線程實例的私有全局變量。
  • 同一個線程運行函數的不同運行實例,他們的TLS是不同的。在這個點上TLS和線程的關系有點類似棧變量和函數的關系。
  • 棧變量在函數退出時會消失,TLS也會在線程結束時釋放。Android實現了TLS的方式是在線程棧的頂開辟了一塊區域來存放TLS項,當然這塊區域不再受線程棧的控制。
  • TLS內存區域按數組方式管理,每個數組元素稱為一個slot。Android 4.4中的TLS一共有128 slot,這和Posix中的要求一致(Android 4.2是64個)

6.4.2 TLS注意事項

  • TLS變量的數量有限,使用前要申請一個key,這個key和內部的slot關聯一起,使用完需要釋放。
    申請一個key的函數原型:
int  pthread_key_create(pthread_key_t *key,void (*destructor_function) (void *) );

pthread_key_create()函數成功返回0,參數key中是分配的slot,如果將來放入slot中的對象需要在線程結束的時候由系統釋放,則需要提供一個釋放函數,通過第二個函數destructor_function傳入。

  • 釋放 TLS key的函數原型是:
 int  pthread_key_delete ( pthread_key_t) ;

pthread_key_delete()函數并不檢查當前是否還有線程正在使用這個slot,也不會調用清理函數,只是將slot釋放以供下次調用pthread_key_create()使用。

  • 利用TLS保存數據中函數原型:
  int pthread_setspecific(pthread_key_t key,const void *value) ;
  • 讀取TLS保存數據中的函數原型:
 void * pthread_getsepcific (pthread_key_t key);

6.5 線程的互斥量(Mutex)函數

Linux線程提供了一組函數用于線程間的互斥訪問,Android中的Mutex類實質上是對Linux互斥函數的封裝,互斥量可以理解為一把鎖,在進入某個保護區域前要先檢查是否已經上鎖了。如果沒有上鎖就可以進入,否則就必須等待,進入后現將鎖鎖上,這樣別的線程就無法再進入了,退出保護區后腰解鎖,其它線程才可以繼續使用

6.5.1 Mutex在使用前需要初始化

初始化函數是:

int pthread_mutex_init(pthread_mutext_t *mutex, const pthread_mutexattr_t *attr);

成功后函數返回0,metex被初始化成未鎖定的狀態。如果參數attr為NULL,則使用缺省的屬性MUTEX_TYPE-BITS_NORMAL。
互斥量的屬性主要有兩種,類型type和范圍scope,設置和獲取屬性的函數如下:

int  pthread_mutexattr_settype (pthread_mutexattr_t * attr, type);
int  pthread_mutexattr_gettype (const pthread_mutexattr_t * attr, int *type);
int  pthread_mutexattr_getpshared(pthread_mutexattr_t *attr, int *pshared );
int  pthread_mutexattrattr_ setpshared (pthread_mutexattr_t *attr,int  pshared);

互斥量Mutex的類型(type) 有3種

  • PTHREAD_MUTEX_NORMAL:該類型的的互斥量不會檢測死鎖。如果線程沒有解鎖(unlock)互斥量的情況下再次鎖定該互斥量,會產生死鎖。如果線程嘗試解鎖由其他線程鎖定的互斥量會產生不確定的行為。如果嘗試解鎖未鎖定的互斥量,也會產生不確定的行為。這是Android目前唯一支持的類型。
  • PTHREAD_MUTEX_ERRORCHECK:此類型的互斥量可提供錯誤檢查。如果線程在沒有解鎖互斥量的情況下嘗試重新鎖定該互斥量,或者線程嘗試解鎖的互斥量由其他線程鎖定。Android目前不支持這種類型
  • PTHREAD_MUTEX_RECURSIVE。如果線程沒有解鎖互斥量的情況下重新鎖定該互斥量,可成功鎖定該互斥量,不會產生死鎖情況,但是多次鎖定該互斥量需要進行相同次數的解鎖才能釋放鎖,然后其他線程才能獲取該互斥量。如果線程嘗試解鎖的互斥量已經由其他線程鎖定,則會返回錯誤。如果線程嘗試解鎖還未鎖定的互斥量,也會返回錯誤。Android目前不支持這種類型 。

互斥量Mutex的作用范圍(scope) 有2種

  • PTHREAD_PROCESS_PRIVATE:互斥量的作用范圍是進程內,這是缺省屬性。
  • PTHREAD_PROCESS_SHARED:互斥量可以用于進程間線程的同步。Android文檔中說不支持這種屬性,但是實際上支持,在audiofliger和surfacefliger都有用到,只不過在持有鎖的進程意外死亡的情況下,互斥量(Mutex)不能釋放掉,這是目前實現的一個缺陷。

6.6 線程的條件量(Condition)函數

6.6.1 為什么需要條件量Condition函數

  • 條件量Condition是為了解決一些更復雜的同步問題而設計的??紤]這樣的一種情況,A和B線程不但需要互斥訪問某個區域,而且線程A還必須等待線程B的運行結果。如果僅使用互斥量進行保護,在線程B先運行的的情況下沒有問題。但是如果線程A先運行,拿到互斥量的鎖,往下忘無法進行。
  • 條件量就是解決這類問題的。在使用條件量的情況下,如果線程A先運行,得到鎖以后,可以使用條件量的等待函數解鎖并等待,這樣線程B得到了運行的機會。線程B運行完以后通過條件量的信號函數喚醒等待的線程A,這樣線程A的條件也滿足了,程序就能繼續執行力額。

6.6.2 Condition函數

    1. 條件量在使用前需要先初始化,函數原型是:
int  pthread_cond_init(pthread_cond_t *cond, const pthread_condattr *attr);

使用完需要銷毀,函數原型是:

int  pthread_cond_destroy(pthread_cond_t *cond);

條件量的屬性只有 "共享(share)" 一種,下面是屬性相關函數原型,下面是屬性相關的函數原型:

int  pthread_condattr_init(pthread_condattr_t *attr);
int  pthread_condattr_getpshared(pthread_condattr_t *attr,int *pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,int pshared) 
int pthread_condattr_destroy (pthread_condattr_t *__attr);

"共享(shared)" 屬性的值有兩種

  • PTHREAD_PROCESS_PRIVATE:條件量的作用范圍是進程內,這是缺省的屬性。
  • PTHREAD_PROCESS_SHARED:條件量可以用于進程間線程同步。
  • 2 條件量的等待函數的原型如下:
int pthread_cond_wait (pthread_cond_t *__restrict __cond,pthread_mutex_t *__restrict __mutex);

 int pthread_cond_timedwait (pthread_cond_t *__restrict __cond,pthread_mutex_t *__restrict __mutex, __const struct timespec *__restrict __abstime);

條件量的等待函數會先解鎖互斥量,因此,使用前一定要確保mutex已經上鎖。鎖上后線程將掛起。pthread_cond_timedwait()用在希望線程等待一段時間的情況下,如果時間到了線程就會恢復運行。

  • 3 可以使用函數pthread_cond_signal()來喚醒等待隊列中的一個線程,原型如下:
int pthread_cond_signal (pthread_cond_t *__cond);

也可以通過pthread_cond_broadcast()喚醒所有等待的線程

 int pthread_cond_broadcast (pthread_cond_t *__cond);

6.6.3 Futex同步機制

  • Futex 是 fast userspace mutext的縮寫,意思是快速用戶控件互斥體。這里討論Futex是因為在Android中不但線程函數使用了Futex,甚至一些模塊中也直接使用了Futex作為進程間同步手段,了解Futex的原理有助于我們理解這些模塊的運行機制。
  • Linux從2.5.7開始支持Futex。在類Unix系統開發中,傳統的進程同步機制都是通過對內核對象進行操作來完成,這個內核對象在需要同步的進程中都是可見的。這種同步方法因為涉及用戶態和內核態的切換,效率比較低。使用了傳統的同步機制時,進入臨界區即使沒有其他進程競爭也會切到內核態檢查內核同步對象的狀態,這種不必要的切換明顯降低了程序的執行效率。
  • Futex就是為了解決這個問題而設計的。Futex是一種用戶態和內核態混合的同步機制,使用Futex同步機制,如果用于進程間同步,需要先調用mmap()創建一塊共享內存,Futex變量就位于共享區。同時對Futex變量的操作必須是原子的,當進程駛入進入臨界區或者退出臨界區的時候,首先檢查共享內存中的Futex變量,如果沒有其他進程也申請了使用臨界區,則只修改Futex變量而不再執行系統調用。如果同時有其他進程也申請使用臨界區,還是需要通過系統調用去執行等待或喚醒操作。這樣通過用戶態的Futex變量的控制,減少了進程在用戶態和內核態之間切換的次數,從而最大程度的降低了系統同步的開銷。
    1 Futex的系統調用
    在Linux中,Futex系統調用的定義如下:
#define _NR_futex    240

Fetex系統調用的原型是:

int  futex(int *uaddr, int cp, int val, const struct timespec *timeout, int *uaddr2, int val3);
  • uaddr是Futex變量,一個共享的整數計數器。
  • op表示操作類型,有5中預定義的值,但是在Bionic中只使用了下面兩種:① FUTEX_WAIT,內核將檢查uaddr中家屬器的值是否等于val,如果等于則掛起進程,直到uaddr到達了FUTEX_WAKE調用或者超時時間到。②FUTEXT_WAKE:內核喚醒val個等待在uaddr上的進程。
  • val存放與操作op相關的值
  • timeout用于操作FUTEX_WAIT中,表示等待超時時間。
  • uaddr2和val3很少使用。
    (1) 在Bionic中,提供了兩個函數來包裝Futex系統調用:
extern int  _futex_wait(volatile void *ftx,int val, const struct timespec *timespec );
extern int _futex_wake(volatile void *ftx, int count);

(2) Bionic還有兩個類似的函數,它們的原型如下:

extern int  _futex_wake_ex(volatile void *ftx,int pshared,int val);
extern int  _futex_wait_ex(volatile void *fex,int pshared,int val, const stuct timespec *timeout);

這兩個函數多了一個參數pshared,pshared的值為true 表示wake和wait操作是用于進程間的掛起和喚醒;值為false表示操作于進程內線程的掛起和喚醒。當pshare的值為false時,執行Futex系統調用的操作碼為

FUTEX_WAIT|FUTEX_PRIVATE_FLAG

內核如何檢測到操作有FUTEX_PRIVATE_FLAG標記,能以更快的速度執行七掛起和喚醒操作。
_futex_wait 和_futex_wake函數相當于pshared等于true的情況。
2、Futex的用戶態操作
Futex的系統調用FUTEX_WAIT和FUTEX_WAKE只是用來掛起或者喚醒進程,Futex的同步機制還包括用戶態下的判斷操作。用戶態下的操作沒有固定的函數調用,只是一種檢測共享變量的方法。Futex用于臨界區的算法如下:

  • 首先創建一個全局的整數變量作為Futex變量,如果用于進程間的同步,這個變量必須位于共享內存。Futex變量的初始值為0。
  • 當進程或線程嘗試持有鎖的時候,檢查Futex變量的值是否為0,如果為0,則將Futex變量的值設為1,然后繼續執行;如果不為0,將Futex的值設為2以后,執行FUTEX_WAIT 系統調用進入掛起等待狀態。
  • Futex變量值為0表示無鎖狀態,1表示有鎖無競爭的狀態,2表示有競爭的狀態。
  • 當進程或線程釋放鎖的時候,如果Futex變量的值為1,說明沒有其他線程在等待鎖,這樣講Futex變量的值設為0就結束了;如果Futex變量的值2,說明還有線程等待鎖,將Futex變量值設為0,同時執行FUTEX_WAKE()系統調用來喚醒等待的進程。

對Futex變量操作時,比較和賦值操作必須是原子的。

參考

Android跨進程通信IPC之2——Bionic

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

推薦閱讀更多精彩內容