I/O基本知識
整個I/O操作由應用程序驹饺、文件系統(tǒng)和磁盤共同完成懦砂,應用程序?qū)/O命令發(fā)送到文件系統(tǒng)窿给,文件系統(tǒng)在合適的時機把I/O操作發(fā)送給磁盤拳缠。整個流程的瓶頸在于磁盤I/O覆获,但有時文件系統(tǒng)為了降低磁盤對應用程序的影響马澈,會采用各種方式進行優(yōu)化,因為文件系統(tǒng)的性能變得十分重要弄息。
1痊班、文件系統(tǒng)
文件系統(tǒng),簡單來說既是存儲和組織數(shù)據(jù)的方式摹量。對于Android來說普遍采用的文件系統(tǒng)是Linux的常用的ext4文件系統(tǒng)涤伐,華為在EMUI5.0以后就使用F2FS取代ext4馒胆,谷歌也在最新的旗艦手機Pixel3使用了F2FS文件系統(tǒng)。F2FS文件系統(tǒng)在小文件隨機讀寫方面比ext4更快凝果,不足之處在于可靠性方面出現(xiàn)過一點問題祝迂,隨著谷歌和華為的投入和使用,未來F2FS將成為Android的主流文件系統(tǒng)器净。
應用程序調(diào)用read()方法型雳,系統(tǒng)會通過中斷從用戶空間進入內(nèi)核處理流程,然后經(jīng)過VFS(Virtual File System山害,虛擬文件系統(tǒng))纠俭、具體文件系統(tǒng)、頁緩存Page Cache浪慌。下面是Linux一個通用I/O架構模型
- 虛擬文件系統(tǒng)(VFS)冤荆。它主要用于實現(xiàn)屏蔽具體的文件系統(tǒng),為應用程序的操作提供一個統(tǒng)一的接口权纤。
- 文件系統(tǒng)(File System)钓简。文件系統(tǒng)需要考慮具體文件元數(shù)據(jù)如何組織、目錄和索引結構如何設計汹想,怎么分配和清理數(shù)據(jù)涌庭。
- 頁緩存(Page Cache)。Page Cache就像我們經(jīng)常使用的數(shù)據(jù)緩存欧宜,是文件系統(tǒng)對數(shù)據(jù)的緩存,目的是提升內(nèi)存命中率拴魄。通過/proc/meminfo文件可以查看緩存的內(nèi)存占用情況冗茸,當手機內(nèi)存不足時,系統(tǒng)會回收他們的內(nèi)存匹中,這樣整體I/O的性能就會有所降低夏漱。
2、磁盤
磁盤指的就是系統(tǒng)的存儲設備顶捷。當應用程序要read()的數(shù)據(jù)沒有在頁緩存中挂绰,這時候就需要真正想磁盤發(fā)起I/O請求,這個過程要先經(jīng)過內(nèi)核的通用模塊層服赎、I/O調(diào)度層葵蒂、設備驅(qū)動層,最后才會交給具體的硬件設備處理重虑。
- 通用塊層践付。系統(tǒng)中能夠隨機訪問固定大小數(shù)據(jù)塊(block)的設備成為設備,CD缺厉、磁盤和SSD這些都輸設備永高。通用塊層主要作用是接收上層發(fā)出的磁盤請求隧土,并最終發(fā)出I/O,他的作用類似VFS命爬,提供通用接口曹傀,屏蔽下層實現(xiàn)。
- I/O調(diào)度層饲宛。為了降低真正的磁盤I/O皆愉,我們不能接收到I/O操作后就立即執(zhí)行,而是交由I/O調(diào)度層落萎,根據(jù)不同的算法亥啦,對請求進行合并和排序。
- 塊設備驅(qū)動層练链。 塊設備驅(qū)動層根據(jù)具體的物理設備翔脱,選擇對應的驅(qū)動程序操控硬件設備完成最終的I/O請求。
Android I/O
閃存是手機常用的存儲設備媒鼓,Android手機前幾年通常使用eMMC標準届吁,近年來通常會使用性能更好的UFS2.0/2.1標準。
1绿鸣、文件為什么會損壞:
一個文件的格式或者內(nèi)容疚沐,如果沒有按照應用程序?qū)懭霑r的結果都屬于文件損壞。
- 應用程序潮模。大部分I/O方法都是非原子操作亮蛔,文件的跨進程或多線程寫入、使用一個已經(jīng)關閉的文件描述符fd來操作文件擎厢,它們都有可能導致文件內(nèi)容修改和刪除究流。
- 文件系統(tǒng)。文件系統(tǒng)把數(shù)據(jù)寫入到頁緩存动遭,在合適的時機寫入到磁盤芬探,內(nèi)核崩潰和斷電就有可能導致寫入到磁盤異常和沒有寫入。
- 磁盤厘惦。磁盤屬于電子設備偷仿,內(nèi)容在傳輸過程中存在錯誤的可能,同時閃存的壽命也可能導致文件錯誤宵蕉。
2酝静、I/O為什么會突然變慢:
- 內(nèi)存不足,當手機內(nèi)存不足時羡玛,系統(tǒng)會回收Page Cache和Buffer Cache的內(nèi)存形入,大部分的寫操作會直接落盤,導致性能低下缝左。
- 寫入放大亿遂,閃存重復寫入需要先擦除操作浓若,這個擦除操作是以block塊為基本單元的,一個page的寫入操作會引起整個塊數(shù)據(jù)遷移蛇数。低端機或者使用很久的設備挪钓,由于磁盤碎片多、剩余空間少耳舅,非常容易出現(xiàn)寫入放大的現(xiàn)象碌上。
- 低端機的CPU和閃存性能相對較差,在高負載的情況下容易出現(xiàn)瓶頸浦徊。
系統(tǒng)為了緩解磁盤碎片問題馏予,可以引入fstrim/TRIM機制,在鎖屏盔性、充電等一些時機會觸發(fā)磁盤碎片整理霞丧。
I/O的性能評估
1、I/O性能指標
最核心的指標為吞吐量和IOPS冕香。吞吐量是指單位時間的數(shù)據(jù)讀取或?qū)懭敕逯涤汲ⅲ琁OPS指的是每秒可以讀寫的次數(shù)。
2悉尾、I/O測量
- 使用proc
- 使用strace突那,可以跟蹤I/O相關的系統(tǒng)調(diào)用次數(shù)和耗時
- 使用vmstat
I/O的三種方式
1、標準I/O
應用程序平時用到的read/write操作都屬于標準I/O构眯,也就是緩存I/O(Buffered I/O)愕难。其特點為:
- 對于讀操作來說,當應用程序讀取到某塊數(shù)據(jù)時惫霸,如果這塊數(shù)據(jù)已經(jīng)存放在頁緩存中务漩,那么這塊數(shù)據(jù)之間返回,不需要實際的物理操作它褪。
-
對于寫操作來書,應用程序也會將數(shù)據(jù)寫到頁緩存中去翘悉,數(shù)據(jù)是否被立即寫到磁盤上取決于應用程序所采用的寫操作機制茫打。默認系統(tǒng)采用延遲寫機制,應用程序只需寫入頁緩存妖混,系統(tǒng)負責定期寫入到磁盤老赤。
Page Cache中被修改的內(nèi)存稱為“臟頁”,內(nèi)核通過flush線程定期將數(shù)據(jù)寫入磁盤制市,具體寫入的條件我們可以通過/proc/sys/vm文件或sysctl -a | grep vm命令得到抬旺。如果某些數(shù)據(jù)非常重要,不允許出現(xiàn)丟失的風險祥楣,這個時候可以采用同步寫機制开财,在應用程序中使用sync汉柒、fsync、msync等系統(tǒng)調(diào)用時责鳍,內(nèi)核都會將相應的數(shù)據(jù)寫回到磁盤碾褂。
2、直接I/O
直接I/O訪問文件方式減少了一次數(shù)據(jù)拷貝和一些系統(tǒng)調(diào)用時間历葛,很大程度上降低了CPU的使用率以及內(nèi)存的占用正塌。但是,直接I/O有時候也會對性能產(chǎn)生不良影響:
- 對于讀操作來說恤溶,讀數(shù)據(jù)操作會造成磁盤同步讀乓诽,導致進程需要較長時間才能執(zhí)行完;
- 對于寫操作來說咒程,使用直接I/O也需要同步執(zhí)行鸠天,也會導致程序等待。
3孵坚、mmap
它是通過吧文件映射到進程的地址空間粮宛,帶來的好處有:
- 減少系統(tǒng)調(diào)用。只需一次mmap()系統(tǒng)調(diào)用卖宠,后續(xù)所有調(diào)用像操作內(nèi)存一樣巍杈,而不會出現(xiàn)大量的read/write系統(tǒng)調(diào)用。
- 減少數(shù)據(jù)拷貝扛伍。普通的read()操作需要經(jīng)過兩次拷貝筷畦,而mmap只需要從磁盤拷貝一次,并且由于做過內(nèi)存映射刺洒,也不需要再拷貝回用戶空間鳖宾。
-
可靠性高。mmap把數(shù)據(jù)寫入緩存逆航,跟緩存I/O的延遲寫機制是一樣的鼎文,可以依靠內(nèi)核線程定期寫回磁盤。但是在內(nèi)核崩潰或斷電的時候因俐,同樣可能引起內(nèi)容丟失拇惋,也可以使用msync來強制同步寫。
mmap同樣也存在缺點:
- 虛擬內(nèi)存增大抹剩。mmap會導致虛擬內(nèi)存增大撑帖,mmap一個大文件,有可能出現(xiàn)虛擬內(nèi)存不足而導致OOM澳眷。
-
磁盤延遲胡嘿。mmap通過缺頁中斷向磁盤發(fā)起真正的磁盤I/O,所以如果當前的問題在于磁盤I/O的高延遲钳踊,那么用mmap()消除小小的系統(tǒng)調(diào)用開銷是杯水車薪的衷敌。類重排技術勿侯,就是將Dex中類按照啟動順序重新排列,主要為了減少缺頁中斷造成的磁盤I/O延遲逢享。
mmap比較適合對同一塊區(qū)域頻繁讀寫的情況罐监,用戶日志、數(shù)據(jù)上報都滿足這種場景瞒爬,另外跨進程通訊的時候也是不錯的選擇弓柱。Android跨進程通訊的Binder機制,其內(nèi)部實現(xiàn)就是采用的mmap實現(xiàn)侧但。
利用mmap矢空,Binder在跨進程通訊只需要一次數(shù)據(jù)拷貝,比傳統(tǒng)的Socket禀横、管道等跨進程通訊方式會少一次數(shù)據(jù)拷貝屁药。
多線程阻塞I/O和NIO
1、多線程阻塞I/O
文件讀寫受到I/O性能瓶頸的影響柏锄,在到達一定速度后整體性能就會受到明顯影響酿箭,過多的線程反而導致應用整體性能明顯下降。
2趾娃、NIO
非阻塞NIO將I/O以事件的方式通知缭嫡,的確可以減少線程切換的開銷。其缺點是導致應用程序?qū)崿F(xiàn)變得更復雜抬闷。其實NIO最大作用不是減少讀取文件的耗時妇蛀,而是最大化提升應用整體的CPU利用率怎茫。在CPU繁忙地時候符喝,我們可以將線程等待磁盤I/O的時間來做部分CPU操作嗤瞎。
I/O跟蹤
1芬首、Java Hook
出于穩(wěn)定性的考慮,采用Java Hook的方案将宪,通過動態(tài)代理的方式苞七,在所有I/O相關方法前后加入插裝代碼器予,統(tǒng)計I/O操作相關信息培遵。這個方法存在下列缺點:
- 性能極差浙芙。
- 無法監(jiān)控Native代碼。
- 兼容性差荤懂。
2、Native Hook
Profilo使用PLT Hook方案塘砸,它的性能比GOT Hook要稍好些节仿,不過GOT Hook的兼容性更好一些。
3掉蔬、監(jiān)控內(nèi)容
線上監(jiān)控
通過Native Hook方案可以采集到所有I/O相關的信息廊宪,對于I/O的線上監(jiān)控矾瘾,我們需要進一步抽象出規(guī)則,明確哪些情況可以定義為不良情況箭启,需要上報到后臺壕翩,進而推動開發(fā)去解決。
1傅寡、主線程I/O
有時候I/O的寫入會突然放大放妈,所以盡量不要在主線程上操作,線上也經(jīng)常發(fā)現(xiàn)一些I/O操作明明數(shù)據(jù)量不大荐操,但是最后還是出現(xiàn)ANR芜抒。如果將主線程的所有I/O都收集起來,數(shù)據(jù)量會非常大托启,所以可以加上“連續(xù)讀寫時間超過100毫秒”這樣的條件宅倒,之所以連續(xù)讀寫,是因為不少情況這是打開了文件句柄屯耸,但不是一次讀寫完的拐迁。
2、讀寫B(tài)uffer過小
文件系統(tǒng)讀寫是以Block為單位的疗绣,對于磁盤是以Page為單位讀寫线召,如果我們的Buffer太小會導致對此無用的系統(tǒng)調(diào)用和內(nèi)存拷貝,導致read/write次數(shù)增多持痰,從而影響性能灶搜。Buffer的大小對文件的讀寫的耗時有非常大的影響,耗時減小主要得益于系統(tǒng)圖調(diào)用于內(nèi)存拷貝的優(yōu)化工窍,Buffer的大小一般推薦使用4KB以上割卖。
3、重復讀
如果頻繁的讀取某個文件患雏,并且這個文件一直沒有被寫入跟新鹏溯,我們可以通過緩存來提升性能,不過未來減少上報量淹仑,通常增加幾個條件:
- 重復讀取次數(shù)超過3次丙挽,并且讀取的內(nèi)容相同。
- 讀取期間文件內(nèi)容沒有被更新匀借,也沒有發(fā)生過write
4颜阐、資源泄漏
資源泄漏是指打開資源包括文件、cursor等沒有及時關閉吓肋,從而引起的泄漏凳怨。利用Android 框架中的StrictMode實現(xiàn)監(jiān)控資源泄漏,StrictMode利用CloseGuard.java類在很多系統(tǒng)代碼已經(jīng)預制了埋點,我們可以增加更多的埋點肤舞,據(jù)圖步驟如下:
- 利用反射紫新,吧CloseGuard中的ENABLED的值為true。
- 利用動態(tài)代理李剖,把REPORTER替換成我們定義的proxy芒率。
I/O啟動優(yōu)化
- 對大文件使用mmap或者NIO方式。
- 安裝包不壓縮篙顺。對啟動過程需要的文件偶芍,指定不壓縮,這樣會加快啟動速度慰安,但會帶來安裝包體積增大的問題腋寨。
- Buffer復用。重用技巧化焕,如Okio很大程度上減少CPU和內(nèi)存的消耗萄窜。
- 存儲結構和算法優(yōu)化。從存儲結構和算法優(yōu)化方面減少甚至去除I/O操作撒桨。