一逆趋、基礎(chǔ)概念
Linux的進(jìn)程空間是相互隔離的。
Linux將內(nèi)存空間在邏輯上劃分為內(nèi)核空間與用戶空間晒奕。Linux 操作系統(tǒng)和驅(qū)動程序運行在內(nèi)核空間闻书,應(yīng)用程序運行在用戶空間,為了保證內(nèi)核安全脑慧,它們是隔離的魄眉。內(nèi)核空間可以訪問所有內(nèi)存空間,而用戶空間不能訪問內(nèi)核空間闷袒。
用戶程序只能通過系統(tǒng)調(diào)用陷入內(nèi)核態(tài)坑律,從而訪問內(nèi)核空間。系統(tǒng)調(diào)用主要通過 copy_to_user() 和 copy_from_user() 實現(xiàn)囊骤,copy_to_user() 用于將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間晃择,copy_from_user() 用于將數(shù)據(jù)從用戶空間拷貝到內(nèi)核空間冀值。
二、Binder解析
Binder是Android上的一種進(jìn)程間通信機制宫屠,它基于Client-Server模式實現(xiàn)列疗,由BinderDriver、ServiceManager浪蹂、Client和Server四個模塊組成抵栈。Binder相較于Socket等傳統(tǒng)IPC方式的優(yōu)勢:
安全性好:為發(fā)送方添加UID/PID身份信息
性能更佳:傳輸過程只要一次數(shù)據(jù)拷貝,而Socket坤次、管道等傳統(tǒng)IPC手段都至少需要兩次數(shù)據(jù)拷貝
Binder四大模塊:
Binder Driver位于內(nèi)核空間中古劲,主要負(fù)責(zé)Binder通信的建立,以及其在進(jìn)程間的傳遞和Binder引用計數(shù)管理/數(shù)據(jù)包的傳輸?shù)如趾铩6鳦lient與Server之間的跨進(jìn)程通信則統(tǒng)一通過Binder Driver處理轉(zhuǎn)發(fā)产艾。
對于Client來說,只需要知道自己要使用的Binder的名字洛波,然后通過0號引用去訪問ServerManager獲取目標(biāo)Binder的引用胰舆,得到引用后就可以像普通方法那樣調(diào)用Binder實體的方法骚露。
Server在生成一個Binder實體的同時會為其綁定一個別名并將別名傳遞給Binder Driver蹬挤,Binder Driver接收后如果發(fā)現(xiàn)是新增的Binder,那么就會為其在內(nèi)核空間中創(chuàng)建相應(yīng)的Binder實體節(jié)點棘幸,然后Binder Driver將該節(jié)點的引用傳遞給ServerManager焰扳,ServerManager收到后再將該Binder的別名和引用插入到一張數(shù)據(jù)表中,這跟DNS中存儲的域名到IP地址的映射原理類似误续。
ServerManager也是個標(biāo)準(zhǔn)的Server,并且在Android中約定其在Binder通信的過程中唯一標(biāo)識永遠(yuǎn)是0吨悍,也就是前面提到的0號引用。
Android系統(tǒng)啟動過程中SystemServer會向BinderDriver注冊ServiceManager蹋嵌,BinderDriver自動為ServiceManager創(chuàng)建Binder實體育瓜。所有在這之后啟動的應(yīng)用進(jìn)程都會持有這個Binder的句柄,為0號引用栽烂,即所有用戶進(jìn)程的0號引用都指向該Binder躏仇。ActivityManagerService、PackageManagerService等系統(tǒng)服務(wù)都是通過Binder機制與應(yīng)用進(jìn)行雙向通信腺办。
傳統(tǒng)的IPC方式:
* 發(fā)送方先將準(zhǔn)備好的數(shù)據(jù)存放在緩存區(qū)中
* 然后通過系統(tǒng)調(diào)用進(jìn)入內(nèi)核中焰手,內(nèi)核服務(wù)程序在內(nèi)核空間分配內(nèi)存,將數(shù)據(jù)從發(fā)送方緩存區(qū)復(fù)制到內(nèi)核緩存區(qū)中怀喉。
* 接收方讀數(shù)據(jù)時也要提供一塊緩存區(qū)书妻,內(nèi)核將數(shù)據(jù)從內(nèi)核緩存區(qū)拷貝到接收方提供的緩存區(qū)中。
* 這種存儲-轉(zhuǎn)發(fā)機制有兩個缺陷:
* 首先是效率低下躬拢,需要做兩次拷貝:用戶空間->內(nèi)核空間->用戶空間躲履。Linux使用copy_from_user()和copy_to_user()實現(xiàn)這兩個跨空間拷貝见间,在此過程中如果使用了高端內(nèi)存(high memory),這種拷貝需要臨時建立/取消頁面映射崇呵,造成性能損失缤剧。
* 其次是接收數(shù)據(jù)的緩存要由接收方提供,可接收方不知道到底要多大的緩存才夠用域慷,只能開辟盡量大的空間或先調(diào)用API接收消息頭獲得消息體大小荒辕,再開辟適當(dāng)?shù)目臻g接收消息體。兩種做法都有不足犹褒,不是浪費空間就是浪費時間抵窒。
顯然,Linux上的跨進(jìn)程通信需要內(nèi)核空間做支持叠骑。傳統(tǒng)的跨進(jìn)程通信方式有Socket李皇、信號量、管道宙枷、內(nèi)存共享等掉房,他們都屬于Linux內(nèi)核,但Android上的Binder并不屬于Linux內(nèi)核慰丛,那么Binder如何實現(xiàn)IPC呢卓囚?答案是 Loadable Kernel Module查看wiki,
Android利用Linux的動態(tài)內(nèi)核可加載模塊機制(Loadable Kernel Module诅病,LKM)哪亿,建立Binder Driver掛載為動態(tài)內(nèi)核,然后通過Binder Driver以mmap的方式將內(nèi)核空間與接收方的用戶空間進(jìn)行內(nèi)存映射贤笆,于是只需要從發(fā)送方的用戶空間拷貝數(shù)據(jù)到內(nèi)核空間中蝇棉,就實現(xiàn)了一次數(shù)據(jù)拷貝完成進(jìn)程間通信。使用mmap建立內(nèi)核空間跟用戶空間的映射后芥永,同一份物理內(nèi)存篡殷,既可以在用戶空間用虛擬地址訪問,也可以在內(nèi)核空間用虛擬地址訪問埋涧。所以mmap的本質(zhì)是讓用戶空間中的一塊虛擬地址與內(nèi)核空間中的一塊虛擬地址指向同一塊物理地址板辽。
Android應(yīng)用在進(jìn)程啟動之初會創(chuàng)建一個單例的ProcessState對象,其構(gòu)造函數(shù)執(zhí)行時會同時完成binder mmap飞袋,為進(jìn)程分配一塊內(nèi)存戳气,專門用于Binder通信。
匿名Binder
在ServiceManager中注冊過的Binder都叫實名Binder巧鸭。當(dāng)Client與Server通過實名Binder建立好Binder連接后瓶您,Server還可以通過這個連接將新的Binder實體封裝進(jìn)數(shù)據(jù)包傳遞給Client,這個被傳遞的就叫做匿名Binder,匿名Binder依然會在Binder Driver中生成實體節(jié)點呀袱,但不會在ServiceManager中注冊贸毕。
匿名Binder為通信雙方建立起一條私密通道,只要Server沒有把匿名Binder發(fā)給別的進(jìn)程夜赵,別的進(jìn)程就無法通過窮舉或猜測等任何方式獲得該Binder的引用明棍,向該Binder發(fā)送請求。
Binder線程(參考資料)
Binder通信實際上是位于不同進(jìn)程中的線程之間的通信寇僧。假如進(jìn)程S是Server端摊腋,提供Binder實體,線程T1從Client進(jìn)程C1中通過Binder的引用向進(jìn)程S發(fā)送請求嘁傀。S為了處理這個請求需要啟動線程T2兴蒸,而此時線程T1處于接收返回數(shù)據(jù)的等待狀態(tài)。T2處理完請求就會將處理結(jié)果返回給T1细办,T1被喚醒得到處理結(jié)果橙凳。在這過程中,T2仿佛T1在進(jìn)程S中的代理笑撞,代表T1執(zhí)行遠(yuǎn)程任務(wù)岛啸,而給T1的感覺就是象穿越到S中執(zhí)行一段代碼又回到了C1。為了使這種穿越更加真實茴肥,驅(qū)動會將T1的一些屬性賦給T2坚踩,特別是T1的優(yōu)先級nice,這樣T2會使用和T1類似的時間完成任務(wù)炉爆。很多資料會用‘線程遷移’來形容這種現(xiàn)象堕虹,容易讓人產(chǎn)生誤解卧晓。一來線程根本不可能在進(jìn)程之間跳來跳去芬首,二來T2除了和T1優(yōu)先級一樣,其它沒有相同之處逼裆,包括身份郁稍,打開文件,棧大小胜宇,信號處理耀怜,私有數(shù)據(jù)等。
對于Server進(jìn)程S桐愉,可能會有許多Client同時發(fā)起請求财破,為了提高效率往往開辟線程池并發(fā)處理收到的請求。怎樣使用線程池實現(xiàn)并發(fā)處理呢从诲?這和具體的IPC機制有關(guān)左痢。拿socket舉例,Server端的socket設(shè)置為偵聽模式,有一個專門的線程使用該socket偵聽來自Client的連接請求俊性,即阻塞在accept()上略步。這個socket就象一只會生蛋的雞,一旦收到來自Client的請求就會生一個蛋 – 創(chuàng)建新socket并從accept()返回定页。偵聽線程從線程池中啟動一個工作線程并將剛下的蛋交給該線程趟薄。后續(xù)業(yè)務(wù)處理就由該線程完成并通過這個單與Client實現(xiàn)交互。
可是對于Binder來說典徊,既沒有偵聽模式也不會下蛋杭煎,怎樣管理線程池呢?一種簡單的做法是卒落,不管三七二十一岔帽,先創(chuàng)建一堆線程,每個線程都用BINDER_WRITE_READ命令讀Binder导绷。這些線程會阻塞在驅(qū)動為該Binder設(shè)置的等待隊列上犀勒,一旦有來自Client的數(shù)據(jù)驅(qū)動會從隊列中喚醒一個線程來處理。這樣做簡單直觀妥曲,省去了線程池贾费,但一開始就創(chuàng)建一堆線程有點浪費資源。于是Binder協(xié)議引入了專門命令或消息幫助用戶管理線程池檐盟,包括:
· INDER_SET_MAX_THREADS
· BC_REGISTER_LOOP
· BC_ENTER_LOOP
· BC_EXIT_LOOP
· BR_SPAWN_LOOPER
首先要管理線程池就要知道池子有多大褂萧,應(yīng)用程序通過INDER_SET_MAX_THREADS告訴驅(qū)動最多可以創(chuàng)建幾個線程。以后每個線程在創(chuàng)建葵萎,進(jìn)入主循環(huán)导犹,退出主循環(huán)時都要分別使用BC_REGISTER_LOOP,BC_ENTER_LOOP羡忘,BC_EXIT_LOOP告知驅(qū)動谎痢,以便驅(qū)動收集和記錄當(dāng)前線程池的狀態(tài)。每當(dāng)驅(qū)動接收完數(shù)據(jù)包返回讀Binder的線程時卷雕,都要檢查一下是不是已經(jīng)沒有閑置線程了节猿。如果是,而且線程總數(shù)不會超出線程池最大線程數(shù)漫雕,就會在當(dāng)前讀出的數(shù)據(jù)包后面再追加一條BR_SPAWN_LOOPER消息滨嘱,告訴用戶線程即將不夠用了,請再啟動一些浸间,否則下一個請求可能不能及時響應(yīng)太雨。新線程一啟動又會通過BC_xxx_LOOP告知驅(qū)動更新狀態(tài)。這樣只要線程沒有耗盡魁蒜,總是有空閑線程在等待隊列中隨時待命囊扳,及時處理請求煤墙。
關(guān)于工作線程的啟動,Binder驅(qū)動還做了一點小小的優(yōu)化宪拥。當(dāng)進(jìn)程P1的線程T1向進(jìn)程P2發(fā)送請求時仿野,驅(qū)動會先查看一下線程T1是否也正在處理來自P2某個線程請求但尚未完成(沒有發(fā)送回復(fù))。這種情況通常發(fā)生在兩個進(jìn)程都有Binder實體并互相對發(fā)時請求時她君。假如驅(qū)動在進(jìn)程P2中發(fā)現(xiàn)了這樣的線程脚作,比如說T2,就會要求T2來處理T1的這次請求缔刹。因為T2既然向T1發(fā)送了請求尚未得到返回包球涛,說明T2肯定(或?qū)┳枞谧x取返回包的狀態(tài)。這時候可以讓T2順便做點事情校镐,總比等在那里閑著好亿扁。而且如果T2不是線程池中的線程還可以為線程池分擔(dān)部分工作,減少線程池使用率鸟廓。
三从祝、mmap擴展
Linux的三種IO方式:標(biāo)準(zhǔn)IO、直接IO引谜、mmap牍陌。
標(biāo)準(zhǔn)IO
應(yīng)用程序平時使用的read()、write()都屬于標(biāo)準(zhǔn)IO员咽,在發(fā)起讀寫操作后其實是往內(nèi)核空間的頁緩存讀寫數(shù)據(jù)毒涧。對于寫操作,系統(tǒng)默認(rèn)是延遲寫入機制贝室,頁緩存的數(shù)據(jù)會由內(nèi)核在合適的時機寫入磁盤契讲。
* 用戶發(fā)起 write 操作
* 操作系統(tǒng)查找頁緩存
a.若未命中,則產(chǎn)生缺頁異常滑频,然后創(chuàng)建頁緩存捡偏,將用戶傳入的內(nèi)容寫入頁緩存
b.若命中,則直接將用戶傳入的內(nèi)容寫入頁緩存
* 用戶 write 調(diào)用完成
* 頁被修改后成為臟頁误趴,操作系統(tǒng)有兩種機制將臟頁寫回磁盤
a.用戶手動調(diào)用 fsync()
b.由 pdflush 進(jìn)程定時將臟頁寫回磁盤
可以看出write過程中有兩次數(shù)據(jù)拷貝霹琼,第一次是從內(nèi)存空間寫入內(nèi)核空間务傲,第二次是內(nèi)核將頁緩存數(shù)據(jù)寫入磁盤凉当。
知識擴展:
相對于機械硬盤,SSD 存儲還有一個“寫入放大”的問題售葡。這個問題主要和 SSD 存儲的物理結(jié)構(gòu)有關(guān)看杭。
當(dāng) SSD 被全部寫過一遍之后,再寫入的數(shù)據(jù)是不可以直接更新挟伙,只可以通過覆蓋重寫楼雹,在覆蓋之前需要先擦除數(shù)據(jù)。
但寫入的最小單位是 Page,擦除的最小單位是 Block贮缅,而 Block 遠(yuǎn)大于 Page榨咐,所以在寫入新數(shù)據(jù)時就需要先把 Block 上的數(shù)據(jù)讀出來和要寫入的數(shù)據(jù)合并在一起,再把 Block 擦除谴供,最后把讀出來的數(shù)據(jù)重新寫入到存儲上块茁,這樣導(dǎo)致實際寫入的數(shù)據(jù)可能遠(yuǎn)遠(yuǎn)大于最開始需要寫入的數(shù)據(jù)。
直接IO
應(yīng)用程序直接讀寫磁盤桂肌。Android并沒有提供直接IO的JAVA API数焊。
mmap
mmap是操作系統(tǒng)中一種內(nèi)存映射的方法。
內(nèi)存映射:就是將用戶空間的一塊內(nèi)存區(qū)域映射到內(nèi)核空間崎场。映射關(guān)系建立后佩耳,用戶對這塊內(nèi)存區(qū)域的修改可以直接反應(yīng)到內(nèi)核空間;反之內(nèi)核空間對這段區(qū)域的修改也能直接反應(yīng)到用戶空間谭跨。
mmap通常用在有物理介質(zhì)的文件系統(tǒng)上干厚。使用mmap可以把文件映射到進(jìn)程的地址空間,實現(xiàn)磁盤地址與進(jìn)程虛擬空間地址的對應(yīng)關(guān)系螃宙。
優(yōu)點:
* 減少系統(tǒng)調(diào)用萍诱。只需要一次mmap()的系統(tǒng)調(diào)用,建立映射關(guān)系后就可以像操作內(nèi)存一樣污呼。
* 減少數(shù)據(jù)拷貝次數(shù)裕坊。mmap()只需要一次數(shù)據(jù)拷貝。
缺點:
* 需要占用更多的內(nèi)存燕酷。
Java中提供的內(nèi)存映射實現(xiàn):MappedByteBuffer