Android進(jìn)程間通信之2——Bionic

一、為什么要學(xué)習(xí)Bionic

Bionic庫(kù)是Android的基礎(chǔ)庫(kù)之一,也是連接Android系統(tǒng)和Linux系統(tǒng)內(nèi)核的橋梁,Bionic中包含了很多基本的功能模塊,這些功能模塊基本上都是源于Linux,但是就像青出于藍(lán)而勝于藍(lán),它和Linux還是有一些不一樣的的地方。同時(shí),為了更好的服務(wù)Android,Bionic中也增加了一些新的模塊,由于本次的主題是Androdi的跨進(jìn)程通信,所以了解Bionic對(duì)我們更好的學(xué)習(xí)Android的跨進(jìn)行通信還是很有幫助的。
Android除了使用ARM版本的內(nèi)核外和傳統(tǒng)的x86有所不同,谷歌還自己開發(fā)了Bionic庫(kù),那么谷歌為什么要這樣做那?

二、谷歌為什么使用Bionic庫(kù)

谷歌使用Bionic庫(kù)主要因?yàn)橐韵氯c(diǎn):

  • 1、谷歌沒有使用Linux的GUN Libc,很大一部分原因是因?yàn)镚NU Libc的授權(quán)方式是GPL 授權(quán)協(xié)議有限制,因?yàn)橐坏┸浖惺褂昧薌PL的授權(quán)協(xié)議,該系統(tǒng)所有代碼必須開元。
  • 2、谷歌在BSD的C庫(kù)的基礎(chǔ)上加入了一些Linux特性從而生成了Bionic。Bionic名字的來(lái)源就是BSD和Linux的混合。而且不受限制的開源方式,所以在現(xiàn)代的商業(yè)公司中比較受歡迎。
  • 3、還有就是因?yàn)樾阅艿脑颍驗(yàn)锽ionic的核心設(shè)計(jì)思想就是"簡(jiǎn)單",所以Bionic中去掉了很多高級(jí)功能。這樣Bionic庫(kù)僅為200K左右,是GNU版本體積的一半,這意味著更高的效率和低內(nèi)存的使用,同時(shí)配合經(jīng)過優(yōu)化的Java VM Dalvik才可以保證高的性能。

三、Bionic庫(kù)簡(jiǎn)介

Bionic 音標(biāo)為 bī??nik,翻譯為"仿生"
Bionic包含了系統(tǒng)中最基本的lib庫(kù),包括libc,libm,libdl,libstd++,libthread_db,以及Android特有的鏈接器linker。

四、Bionic庫(kù)的特性

Bionic庫(kù)的特性很多,受篇幅限制,我挑幾個(gè)和大家平時(shí)接觸到的說下

(一)、架構(gòu)

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

(二)、Linux核心頭文件

Bionic自帶一套經(jīng)過清理的Linxu內(nèi)核頭文件,允許用戶控件代碼使用內(nèi)核特有的聲明(如iotcls,常量等)這些頭文件位于目錄:

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

(三)、DNS解析器

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

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

(四)、二進(jìn)制兼容性

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

(五)、Android特性

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

1、訪問系統(tǒng)特性

Android 提供了一個(gè)簡(jiǎn)單的"共享鍵/值 對(duì)" 空間給系統(tǒng)中的所有進(jìn)程,用來(lái)存儲(chǔ)一定數(shù)量的"屬性"。每個(gè)屬性由一個(gè)限制長(zhǎng)度的字符串"鍵"和一個(gè)限制長(zhǎng)度的字符串"值"組成。
頭文件<sys/system_properties.h>中定義了讀系統(tǒng)屬性的函數(shù),也定義了鍵/值對(duì)的最大長(zhǎng)度。

2、Android用戶/組管理

在Android中沒有etc/password和etc/groups 文件。Android使用擴(kuò)展的Linux用戶/組管理特性,以確保進(jìn)程根據(jù)權(quán)限來(lái)對(duì)不同的文件系統(tǒng)目錄進(jìn)行訪問。
Android的策略是:

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

五、Bionic庫(kù)的模塊簡(jiǎn)介

Bionic目錄下一共有5個(gè)庫(kù)和一個(gè)linker程序
5個(gè)庫(kù)分別是:

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

(一)、Libc庫(kù)

Libc是C語(yǔ)言最基礎(chǔ)的庫(kù)文件,它提供了所有系統(tǒng)的基本功能,這些功能主要是對(duì)系統(tǒng)調(diào)用的封裝,Libc是應(yīng)用和Linux內(nèi)核交流的橋梁,主要功能如下:

  • 進(jìn)程管理:包括進(jìn)程的創(chuàng)建、調(diào)度策略和優(yōu)先級(jí)的調(diào)整
  • 線程管理:包括線程的創(chuàng)建和銷毀,線程的同步/互斥等
  • 內(nèi)存管理:包括內(nèi)存分配和釋放等
  • 時(shí)間管理:包括獲取和保存系統(tǒng)時(shí)間、獲取當(dāng)前系統(tǒng)運(yùn)行時(shí)長(zhǎng)等
  • 時(shí)區(qū)管理:包括時(shí)區(qū)的設(shè)置和調(diào)整等
  • 定時(shí)器管理:提供系統(tǒng)的定時(shí)服務(wù)
  • 文件系統(tǒng)管理:提供文件系統(tǒng)的掛載和移除功能
  • 文件管理:包括文件和目錄的創(chuàng)建增刪改
  • 網(wǎng)絡(luò)套接字:創(chuàng)建和監(jiān)聽socket,發(fā)送和接受
  • DNS解析:幫助解析網(wǎng)絡(luò)地址
  • 信號(hào):用于進(jìn)程間通信
  • 環(huán)境變量:設(shè)置和獲取系統(tǒng)的環(huán)境變量
  • Android Log:提供和Android Log驅(qū)動(dòng)進(jìn)行交互的功能
  • Android 屬性:管理一個(gè)共享區(qū)域來(lái)設(shè)置和讀取Android的屬性
  • 標(biāo)準(zhǔn)輸入/輸出:提供格式化的輸入/輸出
  • 字符串:提供字符串的移動(dòng)、復(fù)制和比較等功能
  • 寬字符:提供對(duì)寬字符的支持。

(二)、Libm庫(kù)

Libm 是數(shù)學(xué)函數(shù)庫(kù),提供了常見的數(shù)學(xué)函數(shù)和浮點(diǎn)運(yùn)算功能,但是Android浮點(diǎn)運(yùn)算時(shí)通過軟件實(shí)現(xiàn)的,運(yùn)行速度慢,不建議頻繁使用。

(三)、libdl庫(kù)

libdl庫(kù)原本是用于動(dòng)態(tài)庫(kù)的裝載。很多函數(shù)實(shí)現(xiàn)都是空殼,應(yīng)用進(jìn)程使用的一些函數(shù),實(shí)際上是在linker模塊中實(shí)現(xiàn)。

(四)、libstd++庫(kù)

libstd++ 是標(biāo)準(zhǔn)的C++的功能庫(kù),但是,Android的實(shí)現(xiàn)是非常簡(jiǎn)單的,只是new,delete等少數(shù)幾個(gè)操作符的實(shí)現(xiàn)。

(五)、libthread_db庫(kù)

libthread_db 用來(lái)支持對(duì)多線程中動(dòng)態(tài)庫(kù)的調(diào)試。

(六)、Linker模塊

Linux系統(tǒng)上其實(shí)有兩種并不完全相同的可執(zhí)行文件

  • 一種是靜態(tài)鏈接的可執(zhí)行程序。靜態(tài)可執(zhí)行程序包含了運(yùn)行需要的所有函數(shù),可以不依賴任何外部庫(kù)來(lái)運(yùn)行。
  • 另一種是動(dòng)態(tài)鏈接的可執(zhí)行程序。動(dòng)態(tài)鏈接的可執(zhí)行程序因?yàn)闆]有包含所需的庫(kù)文件,因此相對(duì)于要小很多。

靜態(tài)可執(zhí)行程序用在一些特殊場(chǎng)合,例如,系統(tǒng)初始化時(shí),這時(shí)整個(gè)系統(tǒng)還沒有準(zhǔn)備好,動(dòng)態(tài)鏈接的程序還無(wú)法使用。系統(tǒng)的啟動(dòng)程序Init就是一個(gè)靜態(tài)鏈接的例子。在Android中,會(huì)給程序自動(dòng)加上兩個(gè)".o"文件,分別是"crtbegin_static.c"和"certtend_android.o",這兩個(gè)".o"文件對(duì)應(yīng)的源文件位于bionic/libc/arch-common/bionic目錄下,文件分別是crtbegin.c和certtend.S。_start()函數(shù)就位于cerbegin.c中。

在動(dòng)態(tài)鏈接時(shí),execuve()函數(shù)會(huì)分析可執(zhí)行文件的文件頭來(lái)尋找鏈接器,Linux文件就是ld.so,而Android則是Linker。execuve()函數(shù)將會(huì)將Linker載入到可執(zhí)行文件的空間,然后執(zhí)行Linker的_start()函數(shù)。Linker完成動(dòng)態(tài)庫(kù)的裝載和符號(hào)重定位后再去運(yùn)行真正的可執(zhí)行文件的代碼。

六、Bionic庫(kù)的內(nèi)存管理函數(shù)

(一)內(nèi)存管理函數(shù)

對(duì)于32位的操作系統(tǒng),能使用的最大地址空間是4GB,其中地址空間3GB分配給用戶進(jìn)程使用,但是用戶進(jìn)程并不是在啟動(dòng)時(shí)就獲取了所有的0~3GB地址空間的訪問權(quán)利,而是需要事先向內(nèi)核申請(qǐng)對(duì)模塊地址空間的讀寫權(quán)利。而且申請(qǐng)的只是地址空間而已,此時(shí)并沒有分配真正的物理地址。只有當(dāng)進(jìn)程訪問某個(gè)地址時(shí),如果該地址對(duì)應(yīng)的物理頁(yè)面不存在,則由內(nèi)核產(chǎn)生缺頁(yè)中斷,在中斷中才會(huì)分配物理內(nèi)存并建立頁(yè)表。如果用戶進(jìn)程不需要某塊空間了,可以通過內(nèi)核釋放掉它們,對(duì)應(yīng)的物理內(nèi)存也釋放掉。

但是由于缺頁(yè)中斷會(huì)導(dǎo)致運(yùn)行緩慢,如果頻繁的地由內(nèi)核來(lái)分配和釋放內(nèi)存將會(huì)降低整個(gè)體統(tǒng)的性能,因此,一般操作系統(tǒng)都會(huì)在用戶進(jìn)程中提供地址空間的分配和回收機(jī)制。用戶進(jìn)程中的內(nèi)存管理會(huì)預(yù)先向內(nèi)核申請(qǐng)一塊大的地址空間,稱為堆。當(dāng)用戶進(jìn)程需要分配內(nèi)存時(shí),由內(nèi)存管理器從堆中尋找一塊空閑的內(nèi)存分配給用戶進(jìn)程使用。當(dāng)用戶進(jìn)程釋放某塊內(nèi)存時(shí),內(nèi)存管理器并不會(huì)立刻將它們交給內(nèi)核釋放,而是放入空閑列表中,留待下次分配使用。

內(nèi)存管理器會(huì)動(dòng)態(tài)的調(diào)整堆的大小,如果堆的空間使用完了,內(nèi)存管理器會(huì)向堆內(nèi)存申請(qǐng)更多的地址空間,如果堆中空閑太多,內(nèi)存管理器也會(huì)將一部分空間返給內(nèi)核。

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

dlmalloc是一個(gè)十分流行的內(nèi)存分配器。dlmalloc位于bionic/libc/upstream-dlmalloc下,只有一個(gè)C文件malloc.c。由于本次主題是跨進(jìn)程通信,后續(xù)有時(shí)間就Android的內(nèi)存回收單獨(dú)作為一個(gè)課題去講解,今天就不詳細(xì)說了,就簡(jiǎn)單的說下原理。
dlmalloc的原理:

  • dlmalloc內(nèi)部是以鏈表的形式將"堆"的空閑空間根據(jù)尺寸組織在一起。分配內(nèi)存時(shí)通過這些鏈表能快速地找到合適大小的空閑內(nèi)存。如果不能找到滿足要求的空閑內(nèi)存,dlmalloc會(huì)使用系統(tǒng)調(diào)用來(lái)擴(kuò)大堆空間。
  • dlmalloc內(nèi)存塊被稱為"trunk"。每塊大小要求按地址對(duì)齊(默認(rèn)8個(gè)字節(jié)),因此,trunk塊的大小必須為8的倍數(shù)。
  • dlmalloc用3種不同的的鏈表結(jié)構(gòu)來(lái)組織不同大小的空閑內(nèi)存塊。小于256字節(jié)的塊使用malloc_chunk結(jié)構(gòu),按照大小組織在一起。由于尺寸小于的塊一共有256/8=32,所以一共使用了32個(gè)malloc_chunk結(jié)構(gòu)的環(huán)形鏈表來(lái)組織小于256的塊。大小大于256字節(jié)的塊由結(jié)構(gòu)malloc_tree_chunk組成鏈表管理,這些塊根據(jù)大小組成二叉樹。而更大的尺寸則由系統(tǒng)通過mmap的方式單獨(dú)分配一塊空間,并通過malloc_segment組成的鏈表進(jìn)行管理
  • 當(dāng)dlmalloc分配內(nèi)存時(shí),會(huì)通過查找這些鏈表來(lái)快速找到一塊和要求的尺寸大小最匹配的空閑內(nèi)存塊(這樣做事為了盡量避免內(nèi)存碎片)。如果沒有合適大小的塊,則將一塊大的分成兩塊,一塊分配出去,另一塊根據(jù)大小再加入對(duì)應(yīng)的空閑鏈表中。
  • 當(dāng)dlmalloc釋放內(nèi)存時(shí),會(huì)將相鄰的空閑塊合并成一個(gè)大塊來(lái)減少內(nèi)存碎片。如果空閑塊過多,超過了dlmaloc內(nèi)存的閥值,dlmalloc就開始向系統(tǒng)返回內(nèi)存。
  • dlmalloc除了能管理進(jìn)程的"堆"空間,還能提供私有堆管理,就是在堆外單獨(dú)分配一塊地址空間,由dlmalloc按照同樣的方式進(jìn)行管理。dlmalloc中用來(lái)管理進(jìn)程的"堆"空間的函數(shù),都帶有"dl"前綴,如"dlmalloc","dlfree"等,而私有堆的管理函數(shù)則帶有前綴"msspace_",如"msspace_malloc"

Dalvk虛擬機(jī)中使用了dlmalloc進(jìn)行私有堆管理。

七、線程

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

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

1、pthread的實(shí)現(xiàn)基于Futext,同時(shí)盡量使用簡(jiǎn)單的代碼來(lái)實(shí)現(xiàn)通用操作,特征如下:

pthread_mutex_t,pthread_cond_t類型定義只有4字節(jié)。

  • 支持normal,recursive and error-check 互斥量。考慮到通常大多數(shù)的時(shí)候都使用normal,對(duì)normal分支下代碼流程做了很細(xì)致的優(yōu)化
  • 目前沒有支持讀寫鎖,互斥量的優(yōu)先級(jí)和其他高級(jí)特征。在Android還不需要這些特征,但是在未來(lái)可能會(huì)添加進(jìn)來(lái)。

2、Bionic不支持pthread_cancel(),因?yàn)榧尤胨鼤?huì)使得C庫(kù)文件明顯變大,不太值得,同時(shí)有以下幾點(diǎn)考慮

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

3、不要在pthread_once()的回調(diào)函數(shù)中調(diào)用fork(),這么做會(huì)導(dǎo)致下次調(diào)用pthread_once()的時(shí)候死鎖。而且不能在回調(diào)函數(shù)中拋出一個(gè)C++的異常。

4、不能使用_thread關(guān)鍵詞來(lái)定義線程本地存儲(chǔ)區(qū)。

(二)、創(chuàng)建線程和線程的屬性

1、創(chuàng)建線程

函數(shù)pthread_create()用來(lái)創(chuàng)建線程,原型是:

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

其中,pthread_t在android中等同于long

typedef long pthread_t;
  • 參數(shù)thread是一個(gè)指針,pthread_create函數(shù)成功后,會(huì)將代表線程的值寫入其指向的變量。
  • 參數(shù) args 一般情況下為NULL,表示使用缺省屬性。
  • 參數(shù)start_routine是線程的執(zhí)行函數(shù)
  • 參數(shù)arg是傳入線程執(zhí)行函數(shù)的參數(shù)

若線程創(chuàng)建成功,則返回0,若線程創(chuàng)建失敗,則返回出錯(cuò)編號(hào)。
PS:要注意的是,pthread_create調(diào)用成功后線程已經(jīng)創(chuàng)建完成,但是不會(huì)立刻發(fā)生線程切換。除非調(diào)用線程主動(dòng)放棄執(zhí)行,否則只能等待線程調(diào)度。

2、線程的屬性

結(jié)構(gòu) pthread_atrr_t用來(lái)設(shè)置線程的一些屬性,定義如下:

typedef struct
{
    uint32_t        flags;                
    void *    stack_base;              //指定棧的起始地址
    size_t    stack_size;               //指定棧的大小
    size_t    guard_size;  
    int32_t   sched_policy;           //線程的調(diào)度方式
    int32_t   sched_priority;          //線程的優(yōu)先級(jí)
}

使用屬性時(shí)要先初始化,函數(shù)原型是:

int  pthread_attr_init(pthread_attr_t*  attr)

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

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;                      //普通調(diào)度方式
     attr0->sched_priority=0;                                                 //中等優(yōu)先級(jí)
     return 0;
}

下面介紹每項(xiàng)屬性的含義。

  • 1、flag 用來(lái)表示線程的分離狀態(tài)
    Linux線程有兩種狀態(tài):分離(detch)狀態(tài)和非分離(joinable)狀態(tài),如果線程是非分離狀態(tài)(joinable)狀態(tài),當(dāng)線程函數(shù)退出時(shí)或者調(diào)用pthread_exit()時(shí)都不會(huì)釋放線程所占用的系統(tǒng)資源。只有當(dāng)調(diào)用了pthread_join()之后這些資源才會(huì)釋放。如果是分離(detach)狀態(tài)的線程,這些資源在線程函數(shù)退出時(shí)調(diào)用pthread_exit()時(shí)會(huì)自動(dòng)釋放
  • 2、stack_base: 線程棧的基地址
  • 3、stack_size: 線程棧的大小。基地址和棧的大小。
  • 4、guard_size: 線程的棧溢出保護(hù)區(qū)大小。
  • 5、sched_policy:線程的調(diào)度方式。
    線程一共有3種調(diào)度方式:SCHED_NORMAL,SCHED_FIFO,SCHED_RR。其中SCHED_NORMAL代表分時(shí)調(diào)度策略,SCHED_FIFO代表實(shí)時(shí)調(diào)度策略,先到先服務(wù),一旦占用CPU則一直運(yùn)行,一直運(yùn)行到有更高優(yōu)先級(jí)的任務(wù)到達(dá),或者自己放棄。SCHED_RR代表實(shí)時(shí)調(diào)度策略:時(shí)間片輪轉(zhuǎn),當(dāng)前進(jìn)程時(shí)間片用完,系統(tǒng)將重新分配時(shí)間片,并置于就緒隊(duì)尾
  • 6、sched_priority:線程的優(yōu)先級(jí)

Bionic雖然也實(shí)現(xiàn)了pthread_attr_setscope()函數(shù),但是只支持PTHREAD_SCOP_SYSTEM屬性,也就意味著Android線程將在全系統(tǒng)的范圍內(nèi)競(jìng)爭(zhēng)CPU資源。

3、退出線程的方法

(1)、調(diào)用pthread_exit函數(shù)退出

一般情況下,線程運(yùn)行函數(shù)結(jié)束時(shí)線程才退出。但是如果需要,也可以在線程運(yùn)行函數(shù)中調(diào)用pthread_exit()函數(shù)來(lái)主動(dòng)退出線程運(yùn)行。函數(shù)原型如下:

 void pthread_exit( void * retval) ;

其中參數(shù)retval用來(lái)設(shè)置返回值

(2)、設(shè)置布爾的全局變量

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

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

這種方法實(shí)現(xiàn)起來(lái)簡(jiǎn)單可靠,在編程中經(jīng)常使用。但它的缺點(diǎn)是:如果線程處于掛起等待狀態(tài),這種方法就不適用了。
另外一種方式是使用pthread_kill()函數(shù)。pthread_kill()函數(shù)的作用不是"殺死"一個(gè)線程,而是給線程發(fā)送信號(hào)。函數(shù)如下:

  int pthread_kill(pthread tid,int sig);

即使線程處于掛起狀態(tài),也可以使用pthead_kill()函數(shù)來(lái)給線程發(fā)送消息并使得線程執(zhí)行處理函數(shù),使用pthread_kill()函數(shù)的問題是:線程如果在信號(hào)處理函數(shù)中退出,不方便釋放在線程的運(yùn)行函數(shù)中分配的資源。

(3)、通過管道

更復(fù)雜的方法是:創(chuàng)建一個(gè)管道,在線程運(yùn)行函數(shù)中對(duì)管道"讀端"用select()或epoll()進(jìn)行監(jiān)聽,沒有數(shù)據(jù)則掛起線程,通過管道的"寫端"寫入數(shù)據(jù),就能喚起線程,從而釋放資源,主動(dòng)退出。

4、線程的本地存儲(chǔ)TLS

線程本地存儲(chǔ)(TLS)用來(lái)保存、傳遞和線程有關(guān)的數(shù)據(jù)。例如在前面說道的使用pthread_kill()函數(shù)關(guān)閉線程的例子中,需要釋放的資源可以使用TLS傳遞給信號(hào)處理函數(shù)。

(1)、TLS介紹

TLS在線程實(shí)例中是全局可見的,對(duì)某個(gè)線程實(shí)例而言TLS是這個(gè)線程實(shí)例的私有全局變量。同一個(gè)線程運(yùn)行函數(shù)的不同運(yùn)行實(shí)例,他們的TLS是不同的。在這個(gè)點(diǎn)上TLS和線程的關(guān)系有點(diǎn)類似棧變量和函數(shù)的關(guān)系。棧變量在函數(shù)退出時(shí)會(huì)消失,TLS也會(huì)在線程結(jié)束時(shí)釋放。Android實(shí)現(xiàn)了TLS的方式是在線程棧的頂開辟了一塊區(qū)域來(lái)存放TLS項(xiàng),當(dāng)然這塊區(qū)域不再受線程棧的控制。

TLS內(nèi)存區(qū)域按數(shù)組方式管理,每個(gè)數(shù)組元素稱為一個(gè)slot。Android 4.4中的TLS一共有128 slot,這和Posix中的要求一致(Android 4.2是64個(gè))

(2)、TLS注意事項(xiàng)

TLS變量的數(shù)量有限,使用前要申請(qǐng)一個(gè)key,這個(gè)key和內(nèi)部的slot關(guān)聯(lián)一起,使用完需要釋放。
申請(qǐng)一個(gè)key的函數(shù)原型:

int  pthread_key_create(pthread_key_t *key,void (*destructor_function) (void *) );

pthread_key_create()函數(shù)成功返回0,參數(shù)key中是分配的slot,如果將來(lái)放入slot中的對(duì)象需要在線程結(jié)束的時(shí)候由系統(tǒng)釋放,則需要提供一個(gè)釋放函數(shù),通過第二個(gè)函數(shù)destructor_function傳入。
釋放 TLS key的函數(shù)原型是:

 int  pthread_key_delete ( pthread_key_t) ;

pthread_key_delete()函數(shù)并不檢查當(dāng)前是否還有線程正在使用這個(gè)slot,也不會(huì)調(diào)用清理函數(shù),只是將slot釋放以供下次調(diào)用pthread_key_create()使用。
利用TLS保存數(shù)據(jù)中函數(shù)原型:

 int pthread_setspecific(pthread_key_t key,const void *value) ;

讀取TLS保存數(shù)據(jù)中的函數(shù)原型:

 void * pthread_getsepcific (pthread_key_t key);

5、線程的互斥量(Mutex)函數(shù)

Linux線程提供了一組函數(shù)用于線程間的互斥訪問,Android中的Mutex類實(shí)質(zhì)上是對(duì)Linux互斥函數(shù)的封裝,互斥量可以理解為一把鎖,在進(jìn)入某個(gè)保護(hù)區(qū)域前要先檢查是否已經(jīng)上鎖了。如果沒有上鎖就可以進(jìn)入,否則就必須等待,進(jìn)入后先將鎖鎖上,這樣別的線程就無(wú)法再進(jìn)入了,退出保護(hù)區(qū)后要解鎖,其它線程才可以繼續(xù)使用.

(1)、Mutex在使用前需要初始化

初始化函數(shù)是:

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

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

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:該類型的的互斥量不會(huì)檢測(cè)死鎖。如果線程沒有解鎖(unlock)互斥量的情況下再次鎖定該互斥量,會(huì)產(chǎn)生死鎖。如果線程嘗試解鎖由其他線程鎖定的互斥量會(huì)產(chǎn)生不確定的行為。如果嘗試解鎖未鎖定的互斥量,也會(huì)產(chǎn)生不確定的行為。** 這是Android目前唯一支持的類型 **。
  • PTHREAD_MUTEX_ERRORCHECK:此類型的互斥量可提供錯(cuò)誤檢查。如果線程在沒有解鎖互斥量的情況下嘗試重新鎖定該互斥量,或者線程嘗試解鎖的互斥量由其他線程鎖定。** Android目前不支持這種類型 ** 。
  • PTHREAD_MUTEX_RECURSIVE。如果線程沒有解鎖互斥量的情況下重新鎖定該互斥量,可成功鎖定該互斥量,不會(huì)產(chǎn)生死鎖情況,但是多次鎖定該互斥量需要進(jìn)行相同次數(shù)的解鎖才能釋放鎖,然后其他線程才能獲取該互斥量。如果線程嘗試解鎖的互斥量已經(jīng)由其他線程鎖定,則會(huì)返回錯(cuò)誤。如果線程嘗試解鎖還未鎖定的互斥量,也會(huì)返回錯(cuò)誤。** Android目前不支持這種類型 **

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

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

6、線程的條件量(Condition)函數(shù)

(1)為什么需要條件量Condition函數(shù)

條件量Condition是為了解決一些更復(fù)雜的同步問題而設(shè)計(jì)的。考慮這樣的一種情況,A和B線程不但需要互斥訪問某個(gè)區(qū)域,而且線程A還必須等待線程B的運(yùn)行結(jié)果。如果僅使用互斥量進(jìn)行保護(hù),在線程B先運(yùn)行的的情況下沒有問題。但是如果線程A先運(yùn)行,拿到互斥量的鎖,往下忘無(wú)法進(jìn)行。

條件量就是解決這類問題的。在使用條件量的情況下,如果線程A先運(yùn)行,得到鎖以后,可以使用條件量的等待函數(shù)解鎖并等待,這樣線程B得到了運(yùn)行的機(jī)會(huì)。線程B運(yùn)行完以后通過條件量的信號(hào)函數(shù)喚醒等待的線程A,這樣線程A的條件也滿足了,程序就能繼續(xù)執(zhí)行力額

(2)Condition函數(shù)

條件量在使用前需要先初始化,函數(shù)原型是:

int  pthread_cond_init(pthread_cond_t *cond, const pthread_condattr *attr);

使用完需要銷毀,函數(shù)原型是:

int  pthread_cond_destroy(pthread_cond_t *cond);

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

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:條件量的作用范圍是進(jìn)程內(nèi),這是缺省的屬性。
  • PTHREAD_PROCESS_SHARED:條件量可以用于進(jìn)程間線程同步。

條件量的等待函數(shù)的原型如下:

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

條件量的等待函數(shù)會(huì)先解鎖互斥量,因此,使用前一定要確保mutex已經(jīng)上鎖。鎖上后線程將掛起。pthread_cond_timedwait()用在希望線程等待一段時(shí)間的情況下,如果時(shí)間到了線程就會(huì)恢復(fù)運(yùn)行。
使用函數(shù)pthread_cond_signal()來(lái)喚醒等待隊(duì)列中的一個(gè)線程,原型如下:

int pthread_cond_signal (pthread_cond_t *__cond);

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

 int pthread_cond_broadcast (pthread_cond_t *__cond);

(三)、Futex同步機(jī)制

  • Futex 是 fast userspace mutext的縮寫,意思是快速用戶控件互斥體。這里討論Futex是因?yàn)樵贏ndroid中不但線程函數(shù)使用了Futex,甚至一些模塊中也直接使用了Futex作為進(jìn)程間同步手段,了解Futex的原理有助于我們理解這些模塊的運(yùn)行機(jī)制。
  • Linux從2.5.7開始支持Futex。在類Unix系統(tǒng)開發(fā)中,傳統(tǒng)的進(jìn)程同步機(jī)制都是通過對(duì)內(nèi)核對(duì)象進(jìn)行操作來(lái)完成,這個(gè)內(nèi)核對(duì)象在需要同步的進(jìn)程中都是可見的。這種同步方法因?yàn)樯婕坝脩魬B(tài)和內(nèi)核態(tài)的切換,效率比較低。使用了傳統(tǒng)的同步機(jī)制時(shí),進(jìn)入臨界區(qū)即使沒有其他進(jìn)程競(jìng)爭(zhēng)也會(huì)切到內(nèi)核態(tài)檢查內(nèi)核同步對(duì)象的狀態(tài),這種不必要的切換明顯降低了程序的執(zhí)行效率。
  • Futex就是為了解決這個(gè)問題而設(shè)計(jì)的。Futex是一種用戶態(tài)和內(nèi)核態(tài)混合的同步機(jī)制,使用Futex同步機(jī)制,如果用于進(jìn)程間同步,需要先調(diào)用mmap()創(chuàng)建一塊共享內(nèi)存,F(xiàn)utex變量就位于共享區(qū)。同時(shí)對(duì)Futex變量的操作必須是原子的,當(dāng)進(jìn)程駛?cè)脒M(jìn)入臨界區(qū)或者退出臨界區(qū)的時(shí)候,首先檢查共享內(nèi)存中的Futex變量,如果沒有其他進(jìn)程也申請(qǐng)了使用臨界區(qū),則只修改Futex變量而不再執(zhí)行系統(tǒng)調(diào)用。如果同時(shí)有其他進(jìn)程也申請(qǐng)使用臨界區(qū),還是需要通過系統(tǒng)調(diào)用去執(zhí)行等待或喚醒操作。這樣通過用戶態(tài)的Futex變量的控制,減少了進(jìn)程在用戶態(tài)和內(nèi)核態(tài)之間切換的次數(shù),從而最大程度的降低了系統(tǒng)同步的開銷。

1、Futex的系統(tǒng)調(diào)用

在Linux中,F(xiàn)utex系統(tǒng)調(diào)用的定義如下:

#define _NR_futex    240

(1) Fetex系統(tǒng)調(diào)用的原型是:

int  futex(int *uaddr, int cp, int val, const struct timespec *timeout, int *uaddr2, int val3);
  • uaddr是Futex變量,一個(gè)共享的整數(shù)計(jì)數(shù)器。
  • op表示操作類型,有5中預(yù)定義的值,但是在Bionic中只使用了下面兩種:① FUTEX_WAIT,內(nèi)核將檢查uaddr中家屬器的值是否等于val,如果等于則掛起進(jìn)程,直到uaddr到達(dá)了FUTEX_WAKE調(diào)用或者超時(shí)時(shí)間到。②FUTEXT_WAKE:內(nèi)核喚醒val個(gè)等待在uaddr上的進(jìn)程。
  • val存放與操作op相關(guān)的值
  • timeout用于操作FUTEX_WAIT中,表示等待超時(shí)時(shí)間。
  • uaddr2和val3很少使用。

(1) 在Bionic中,提供了兩個(gè)函數(shù)來(lái)包裝Futex系統(tǒng)調(diào)用:

extern int  _futex_wait(volatile void *ftx,int val, const struct timespec *timespec );
extern int _futex_wake(volatile void *ftx, int count);

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

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

這兩個(gè)函數(shù)多了一個(gè)參數(shù)pshared,pshared的值為true 表示wake和wait操作是用于進(jìn)程間的掛起和喚醒;值為false表示操作于進(jìn)程內(nèi)線程的掛起和喚醒。當(dāng)pshare的值為false時(shí),執(zhí)行Futex系統(tǒng)調(diào)用的操作碼為

FUTEX_WAIT|FUTEX_PRIVATE_FLAG

內(nèi)核如何檢測(cè)到操作有FUTEX_PRIVATE_FLAG標(biāo)記,能以更快的速度執(zhí)行七掛起和喚醒操作。
_futex_wait 和_futex_wake函數(shù)相當(dāng)于pshared等于true的情況。
(3) 在Bionic中,提供了兩個(gè)函數(shù)來(lái)包裝Futex系統(tǒng)調(diào)用:

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()相當(dāng)于 _futex_wake(),而 _futex_system4()相當(dāng)于 _futex_wait()。這兩個(gè)函數(shù)與前面的區(qū)別是能指定操作碼op作為參數(shù)。操作碼可以是FUTEX_WAIT_FUTEX_WAKE或者它們和FUTEX_PRIVATE_FLAG的組合。

2、Futex的用戶態(tài)操作

Futex的系統(tǒng)調(diào)用FUTEX_WAIT和FUTEX_WAKE只是用來(lái)掛起或者喚醒進(jìn)程,F(xiàn)utex的同步機(jī)制還包括用戶態(tài)下的判斷操作。用戶態(tài)下的操作沒有固定的函數(shù)調(diào)用,只是一種檢測(cè)共享變量的方法。Futex用于臨界區(qū)的算法如下:

  • 首先創(chuàng)建一個(gè)全局的整數(shù)變量作為Futex變量,如果用于進(jìn)程間的同步,這個(gè)變量必須位于共享內(nèi)存。Futex變量的初始值為0。
  • 當(dāng)進(jìn)程或線程嘗試持有鎖的時(shí)候,檢查Futex變量的值是否為0,如果為0,則將Futex變量的值設(shè)為1,然后繼續(xù)執(zhí)行;如果不為0,將Futex的值設(shè)為2以后,執(zhí)行FUTEX_WAIT 系統(tǒng)調(diào)用進(jìn)入掛起等待狀態(tài)。
  • Futex變量值為0表示無(wú)鎖狀態(tài),1表示有鎖無(wú)競(jìng)爭(zhēng)的狀態(tài),2表示有競(jìng)爭(zhēng)的狀態(tài)。
  • 當(dāng)進(jìn)程或線程釋放鎖的時(shí)候,如果Futex變量的值為1,說明沒有其他線程在等待鎖,這樣講Futex變量的值設(shè)為0就結(jié)束了;如果Futex變量的值2,說明還有線程等待鎖,將Futex變量值設(shè)為0,同時(shí)執(zhí)行FUTEX_WAKE()系統(tǒng)調(diào)用來(lái)喚醒等待的進(jìn)程。

對(duì)Futex變量操作時(shí),比較和賦值操作必須是原子的。

參考文獻(xiàn)

Android跨進(jìn)程通信IPC之2——Bionic

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

推薦閱讀更多精彩內(nèi)容