Bionic 簡介

 

1、爲何要學習Bionic

 

Bionic庫是Android的基礎庫之一,也是鏈接Android系統和Linux系統內核的橋樑,Bionic中包含了不少基本的功能模塊,這些功能模塊基本上都是源於Linux,可是就像青出於藍而勝於藍,它和Linux仍是有一些不同的的地方。同時,爲了更好的服務Android,Bionic中也增長了一些新的模塊,因爲本次的主題是Androdi的跨進程通訊,因此瞭解Bionic對咱們更好的學習Android的跨進行通訊仍是頗有幫助的。android

Android除了使用ARM版本的內核外和傳統的x86有所不一樣,谷歌還本身開發了Bionic庫,那麼谷歌爲何要這樣作那?算法

2、谷歌爲何使用Bionic庫

谷歌使用Bionic庫主要由於如下三點:編程

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

3、Bionic庫簡介

 

Bionic 音標爲 bīˈänik,翻譯爲"仿生"數組

Bionic包含了系統中最基本的lib庫,包括libc,libm,libdl,libstd++,libthread_db,以及Android特有的連接器linker。安全

4、Bionic庫的特性

Bionic庫的特性不少,受篇幅限制,我挑幾個和你們平時接觸到的說下服務器

(一)、架構

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

(二)、Linux核心頭文件

Bionic自帶一套通過清理的Linxu內核頭文件,容許用戶控件代碼使用內核特有的聲明(如iotcls,常量等)這些頭文件位於目錄:多線程

 

bionic/libc/kernel/common
bionic/libc/kernel/arch-arm
bionic/libc/kernel/arch-x86
bionic/libc/kernel/arch-mips架構

(三)、DNS解析器

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

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

(四)、二進制兼容性

因爲Bionic不與GNU C庫、ucLibc,或者任何已知的Linux C相兼容。因此意味着不要指望使用GNU C庫頭文件編譯出來的模塊可以正常地動態連接到Bionic

(五)、Android特性

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

一、訪問系統特性

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

二、Android用戶/組管理

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

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

5、Bionic庫的模塊簡介

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

 
  • 一、libc
  • 二、libm
  • 三、libdl
  • 四、libstd++
  • 五、libthread_db

(一)、Libc庫

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

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

(二)、Libm庫

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

(三)、libdl庫

libdl庫本來是用於動態庫的裝載。不少函數實現都是空殼,應用進程使用的一些函數,其實是在linker模塊中實現。

(四)、Libm庫

libstd++ 是標準的C++的功能庫,可是,Android的實現是很是簡單的,只是new,delete等少數幾個操做符的實現。

(五)、libthread_db庫

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

(六)、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完成動態庫的裝載和符號重定位後再去運行真正的可執行文件的代碼。

6、Bionic庫的內存管理函數

(一)內存管理函數

 

對於32位的操做系統,能使用的最大地址空間是4GB,其中地址空間03GB分配給用戶進程使用,地址空間3GB4GB由內核使用,可是用戶進程並非在啓動時就獲取了全部的0~3GB地址空間的訪問權利,而是須要事先向內核申請對模塊地址空間的讀寫權利。並且申請的只是地址空間而已,此時並無分配真是的物理地址。只有當進程訪問某個地址時,若是該地址對應的物理頁面不存在,則由內核產生缺頁中斷,在中斷中才會分配物理內存並創建頁表。若是用戶進程不須要某塊空間了,能夠經過內核釋放掉它們,對應的物理內存也釋放掉。

可是因爲缺頁中斷會致使運行緩慢,若是頻繁的地由內核來分配和釋放內存將會下降整個體統的性能,所以,通常操做系統都會在用戶進程中提供地址空間的分配和回收機制。用戶進程中的內存管理會預先向內核申請一塊打的地址空間,稱爲堆。當用戶進程須要分配內存時,由內存管理器從堆中尋找一塊空閒的內存分配給用戶進程使用。當用戶進程釋放某塊內存時,內存管理器並不會馬上將它們交給內核釋放,而是放入空閒列表中,留待下次分配使用。

 

內存管理器會動態的調整堆的大小,若是堆的空間使用完了,內存管理器會向堆內存申請更多的地址空間,若是堆中空閒太多,內存管理器也會將一部分空間返給內核。

(二) Bionic的內存管理器——dlmalloc

dlmalloc是一個十分流行的內存分配器。dlmalloc位於bionic/libc/upstream-dlmalloc下,只有一個C文件malloc.c。因爲本次主題是跨進程通訊,後續有時間就Android的內存回收單獨做爲一個課題去講解,今天就不詳細說了,就簡單的說下原理。
dlmalloc的原理:

 
  • 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進行私有堆管理。

7、線程

Bionic中的線程管理函數和通用的Linux版本的實現有不少差別,Android根據本身的須要作了不少裁剪工做。

(一)、Bionic線程函數的特性

一、pthread的實現基於Futext,同時儘可能使用簡單的代碼來實現通用操做,特徵以下:

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

二、Bionic不支持pthread_cancel(),由於加入它會使得C庫文件明顯變大,不太值得,同時有如下幾點考慮

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

三、不要在pthread_once()的回調函數中調用fork(),這麼作會致使下次調用pthread_once()的時候死鎖。並且不能在回調函數中拋出一個C++的異常。

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

(二)、建立線程和線程的屬性

一、建立線程

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

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

其中,pthread_t在android中等同於long

typedef long pthread_t;
 
  • 參數thread是一個指針,pthread_create函數成功後,會將表明線程的值寫入其指向的變量。
  • 參數 args 通常狀況下爲NULL,表示使用缺省屬性。
  • 參數start_routine是線程的執行函數
  • 參數arg是傳入線程執行函數的參數

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

二、線程的屬性

結構 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;
}

下面介紹每項屬性的含義。

 
  • 一、flag 用來表示線程的分離狀態
    Linux線程有兩種狀態:分離(detch)狀態和非分離(joinable)狀態,若是線程是非分離狀態(joinable)狀態,當線程函數退出時或者調用pthread_exit()時都不會釋放線程所佔用的系統資源。只有當調用了pthread_join()以後這些資源纔會釋放。若是是分離(detach)狀態的線程,這些資源在線程函數退出時調用pthread_exit()時會自動釋放
  • 二、stack_base: 線程棧的基地址
  • 三、stack_size: 線程棧的大小。基地址和棧的大小。
  • 四、guard_size: 線程的棧溢出保護區大小。
  • 五、sched_policy:線程的調度方式。
    線程一共有3中調度方式:SCHED_NORMAL,SCHED_FIFO,SCHED_RR。其中SCHED_NORMAL表明分時調度策略,SCHED_FIFO表明實時調度策略,先到先服務,一旦佔用CPU則一直運行,一直運行到有更高優先級的任務到達,或者本身放棄。SCHED_RR表明實時調度策略:時間片輪轉,當前進程時間片用完,系統將從新分配時間片,並置於就緒隊尾。
  • 六、sched_priority:線程的優先級。

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

三、退出線程的方法

(1)、調用pthread_exit函數退出

通常狀況下,線程運行函數結束時線程才退出。可是若是須要,也能夠在線程運行函數中調用pthread_exit()函數來主動退出線程運行。函數原型以下:

void pthread_exit( void * retval) ;

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

(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()函數的問題是:線程若是在信號處理函數中退出,不方便釋放在線程的運行函數中分配的資源。

(3)、經過管道

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

四、線程的本地存儲TLS

 

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

(1)、TLS介紹

TLS在線程實例中是全局可見的,對某個線程實例而言TLS是這個線程實例的私有全局變量。同一個線程運行函數的不一樣運行實例,他們的TLS是不一樣的。在這個點上TLS和線程的關係有點相似棧變量和函數的關係。棧變量在函數退出時會消失,TLS也會在線程結束時釋放。Android實現了TLS的方式是在線程棧的頂開闢了一塊區域來存放TLS項,固然這塊區域再也不受線程棧的控制。

 

TLS內存區域按數組方式管理,每一個數組元素稱爲一個slot。Android 4.4中的TLS一共有128 slot,這和Posix中的要求一致(Android 4.2是64個)

(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);

五、線程的互斥量(Mutex)函數

 

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

(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)不能釋放掉,這是目前實現的一個缺陷。

六、線程的條件量(Condition)函數

(1)爲何須要條件量Condition函數

條件量Condition是爲了解決一些更復雜的同步問題而設計的。考慮這樣的一種狀況,A和B線程不但須要互斥訪問某個區域,並且線程A還必須等待線程B的運行結果。若是僅使用互斥量進行保護,在線程B先運行的的狀況下沒有問題。可是若是線程A先運行,拿到互斥量的鎖,往下忘沒法進行。

 

條件量就是解決這類問題的。在使用條件量的狀況下,若是線程A先運行,獲得鎖之後,可使用條件量的等待函數解鎖並等待,這樣線程B獲得了運行的機會。線程B運行完之後經過條件量的信號函數喚醒等待的線程A,這樣線程A的條件也知足了,程序就能繼續執行力額。

(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);

(三)、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變量的控制,減小了進程在用戶態和內核態之間切換的次數,從而最大程度的下降了系統同步的開銷。

一、Futex的系統調用

在Linux中,Futex系統調用的定義以下:

#define _NR_futex    240

(1) 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的狀況。

(3) 在Bionic中,提供了兩個函數來包裝Futex系統調用:

extern int  _futex_syscall3(volatile void *ftx,int pshared,int val);
extern int  _futex_syscall4(volatile void *ftx,int pshared,int val, const struct timespec *timeout);

_futex_syscall3()至關於 _futex_wake(),而 _futex_system4()至關於 _futex_wait()。這兩個函數與前面的區別是能指定操做碼op做爲參數。操做碼能夠是FUTEX_WAIT_FUTEX_WAKE或者它們和FUTEX_PRIVATE_FLAG的組合。

二、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變量操做時,比較和賦值操做必須是原

做者:隔壁老李頭 連接:https://www.jianshu.com/p/25a908c7eefa 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

相關文章
相關標籤/搜索