應用訪問限制
分割槽儲存的目標是保護應用和使用者資料的隱私。這包括保護使用者資訊(例如照片後設資料)、防止應用在未經明確許可的情況下修改或刪除使用者檔案,以及保護下載到“下載”或其他資料夾的敏感使用者文件。
使用分割槽儲存的應用可具有以下訪問許可權級別(實際訪問許可權因實現而異)。
-
對自己的檔案擁有讀取和寫入訪問許可權(沒有許可權限制)
-
對其他應用的媒體檔案擁有讀取訪問許可權(需要具備
READ_EXTERNAL_STORAGE
許可權) -
只有在使用者直接同意的情況下,才允許對其他應用的媒體檔案擁有寫入訪問許可權(系統相簿以及符合“所有檔案訪問許可權”獲取條件的應用除外)
-
對其他應用的外部應用資料目錄沒有讀取或寫入訪問許可權
將分割槽儲存與 FUSE 搭配使用
Android 11 或更高版本支援使用者空間中的檔案系統 (FUSE),這使 MediaProvider 模組可以檢查使用者空間中的檔案操作並根據允許、拒絕或隱去訪問許可權的政策限制對檔案的訪問。分割槽儲存中使用 FUSE 的應用可獲得分割槽儲存的隱私功能以及透過直接檔案路徑訪問檔案的功能(讓 File API 繼續在應用中執行)。
Android 10 對 MediaProvider 執行的檔案訪問強制實施了分割槽儲存規則,但對直接檔案路徑訪問(例如,使用 File API 和 NDK API)則不實施此類規則,因為攔截核心呼叫所需的工作量較大。因此,分割槽儲存中的應用無法使用直接檔案路徑訪問檔案。此限制影響了應用開發者的適應能力,因為需要大量的程式碼更改才能將 File API 訪問重寫為 MediaProvider API 訪問。
FUSE 和 SDCardFS
Android 11 對 FUSE 的支援與 SDCardFS 的棄用無關,但它為以前使用 SDCardFS 的裝置提供了媒體庫替代項。不同的裝置:
- 搭載 Android 11 或更高版本並執行核心版本 5.4 或更高版本的裝置無法使用 SDCardFS。
- 升級到 Android 11 或更高版本的裝置可以基於 SDCardFS 託管 FUSE,以攔截檔案操作並實現隱私目標。
FUSE 效能微調
之前,Android 7 或更低版本中支援 FUSE,其中的外部儲存空間作為 FUSE 裝載。由於這種 FUSE 實現存在效能和死鎖問題,Android 8 引入了 SDCardFS。Android 11 使用經過改進且經過更充分測試的 libfuse 實現重新引入了對 FUSE 的支援,可以透過微調來解決 Android 7 或更低版本中的效能問題。
FUSE 微調包括以下調整項:
-
為
Android/data
和Android/obb
目錄繞過 FUSE,以提升依賴這些目錄的遊戲應用的效能。 -
進行最佳化(例如微調 FUSE 檔案系統的預讀率和髒比率),以確保出色的讀取效能和流暢的媒體播放。
-
使用 FUSE 回寫快取。
-
透過快取許可權來減少系統伺服器的 IPC。
-
對具有“所有檔案訪問許可權”的應用進行最佳化,以加快批次操作的執行速度。
上述調整項可以在 FUSE 裝置和非 FUSE 裝置之間實現同等的效能。例如,對使用 FUSE 且微調後的 Pixel 2 以及使用媒體庫的 Pixel 2 進行測試之後,我們發現檔案路徑訪問和媒體庫之間具有同等的順序讀取效能(例如影片播放)。不過,使用 FUSE 時的順序寫入效能稍差,隨機讀寫可能會慢上一倍。
效能測量結果可能因裝置和具體用例而異。由於 MediaProvider API 能提供最為一致的效能,因此關注效能的應用開發者應該為其應用使用 MediaProvider API。
減輕與 FUSE 相關的效能影響
只有大量使用儲存在外部共享儲存裝置上的檔案的應用才會受到與 FUSE 相關的效能影響。FUSE 會繞過外部專屬儲存空間(包括 android/data 和 android/obb 目錄),而內部儲存空間(如 /data/data,許多應用在這裡儲存資料以確保其獲得加密和保護)不會裝載 FUSE。
-
只需少量使用共享外部儲存空間的應用通常會與有限的一些檔案(通常少於 100 個檔案)互動。這些應用受益於現有的對常用讀寫操作的最佳化,在 Android 11 中應該不會受到任何與 FUSE 相關的效能影響。
-
大量使用共享外部儲存空間的應用通常會執行批次檔案操作,例如列出或移除包含 1000 個檔案的目錄,或者在檔案系統中建立或刪除包含 100 萬個檔案的目錄。在 Android 11 中,批次檔案操作可能會受到 FUSE 的影響,但如果此類應用符合
MANAGE_EXTERNAL_STORAGE
許可權的獲取條件,它們將從 2020 年 10 月更新中包含的效能最佳化項中獲益。
注意:如果批次操作涉及圖片和影片等大型檔案,檔案 I/O 對檔案操作效能的影響可能比 FUSE 效能開銷更大。
為了避免 FUSE 效能開銷,應用可以將資料儲存在外部專屬儲存空間中,或使用 ContentProvider 類中的批次 API 來繞過 FUSE 並獲取效能經過最佳化的路徑。此外,面向 MediaProvider 系統元件的 2020 年 10 月更新還包括針對擁有 MANAGE_EXTERNAL_STORAGE 許可權的檔案管理器和類似應用(如備份/恢復軟體和防毒軟體)的效能最佳化項。
隱私優勢遠超效能劣勢
在已針對 FUSE 進行微調的裝置上,大多數關鍵使用者歷程在 Android 10 和 Android 11 之間的效能同等出色。不過,在針對一組檔案操作執行基準測試時,Android 11 的效能可能比 Android 10 要差。對於 Android 11 中效能較差的檔案訪問模式(例如,隨機讀取或寫入),我們建議使用 MediaProvider API 為應用提供非 FUSE 訪問模式,這是確保可獲取一致的高效能的最佳方案。
注意:雖然 Android 致力於解決影響使用者歷程的效能降低問題,但我們堅信,分割槽儲存在確保隱私性方面的優勢遠超其造成的效能降低問題的影響。
MediaProvider 和 FUSE 更新
MediaProvider 系統元件的行為因 Android 版本而異。
- 在 Android 10 及更低版本中,SDCardFS 是檔案系統,而 MediaProvider 為檔案(如圖片、影片和音樂檔案等)集合提供了一個介面。應用使用 File API 建立一個檔案時,它可以要求 MediaProvider 掃描該檔案並將其記錄在資料庫中。
- 在 Android 11 或更高版本中,SDCardFS 已廢棄,MediaProvider 成為了外部儲存空間的檔案系統處理程式(適用於 FUSE),可使外部儲存空間上的檔案系統和 MediaProvider 資料庫保持一致。作為 FUSE 檔案系統的使用者空間處理程式,MediaProvider 可以攔截核心呼叫並確保檔案操作的隱私安全。
在 Android 11 及更高版本中,MediaProvider 也是一個可在 Android 版本之外更新的模組化系統元件(一個 Mainline 模組)。這意味著,MediaProvider 中存在的效能、隱私或安全問題可以透過 Google Play 商店或合作伙伴提供的其他機制以無線下載方式提交和修復。FUSE 處理程式牽涉的各類資源均可更新,這使得更新可以修復 FUSE 效能降低問題和 bug。FUSE 透傳
Android 12 支援 FUSE 透傳功能,此功能可以最大限度地降低 FUSE 開銷,從而實現可媲美直接訪問下層檔案系統的效能。android12-5.4、android12-5.10 和 android-mainline(僅限測試)核心支援 FUSE 透傳功能,這意味著是否支援此功能取決於裝置使用的核心和裝置搭載的 Android 版本:
- 從 Android 11 升級到 Android 12 的裝置無法支援 FUSE 透傳功能,因為這些裝置的核心已凍結,並且無法遷移到已使用 FUSE 透傳變更正式升級過的核心。
- 釋出時搭載 Android 12 的裝置在使用官方核心時可以支援 FUSE 透傳功能。在此類裝置上,用於實現 FUSE 透傳功能的 Android 框架程式碼已嵌入 MediaProvider Mainline 模組中,該模組會自動升級。未將 MediaProvider 實現為 Mainline 模組的裝置(例如 Android Go 裝置)也可以獲取 MediaProvider 變更,因為這些變更已公開共享。
FUSE 與 SDCardFS
使用者空間中的檔案系統 (FUSE) 是一種機制,可讓核心(FUSE 驅動程式)將 FUSE 檔案系統中執行的操作外包給使用者空間程式(FUSE 守護程式),由其來實現操作。Android 11 廢棄了 SDCardFS,並將 FUSE 作為儲存空間模擬的預設解決方案。作為此變更的一部分,Android 實現了自己的 FUSE 守護程式,用於攔截檔案訪問,強制執行額外的安全和隱私功能,並在執行時操作檔案。
雖然 FUSE 在處理頁面或屬性等可快取的資訊時效果非常理想,但在訪問外部儲存裝置時卻會導致效能下降,這一點在中低端裝置上尤為明顯。導致這些效能下降問題的原因在於,實現 FUSE 檔案系統需要一系列元件協同配合,在 FUSE 驅動程式與 FUSE 守護程式之間的通訊過程中也需要從核心空間多次切換到使用者空間(與此相比,直接訪問下層檔案系統則更加精簡且完全在核心中實現)。
若要減輕這些效能下降問題,應用可以使用拼接來減少資料複製,並使用 ContentProvider API 直接訪問下層檔案系統檔案。即使採取這些措施並進行其他最佳化之後,與直接訪問下層檔案系統相比,使用 FUSE 時的讀取和寫入操作仍會遇到頻寬降低的問題,特別是在隨機讀取操作中更為突出,因為在這種情況下沒有快取或預讀可助一臂之力。透過舊路徑 /sdcard/ 直接訪問儲存空間的應用會繼續出現顯著的效能下降,尤其是在執行 IO 密集型操作時。
SDcardFS 使用者空間請求
使用 SDcardFS 可從核心中移除使用者空間呼叫,從而加快儲存空間模擬和 FUSE 許可權檢查。使用者空間請求遵循以下路徑:使用者空間 → VFS → sdcardfs → VFS → ext4 → 頁面快取/儲存空間。
圖 1. SDcardFS 使用者空間請求
FUSE 使用者空間請求
FUSE 最初用於實現儲存空間模擬並讓應用能夠透明地使用內部儲存空間或外部 SD 卡。使用 FUSE 會產生一些開銷,因為每個使用者空間請求都遵循以下路徑:使用者空間 → VFS → FUSE 驅動程式 → FUSE 守護程式 → VFS → ext4 → 頁面快取/儲存空間。
圖 2. FUSE 使用者空間請求
FUSE 透傳請求
大多數檔案訪問許可權是在檔案開啟時進行檢查,還有一些其他許可權是在對檔案執行讀取和寫入操作時進行檢查。在某些情況下,可能在檔案開啟時知道發出請求的應用對請求的檔案具有完全訪問許可權,因此係統無需繼續將讀取和寫入請求從 FUSE 驅動程式轉發到 FUSE 守護程式(因為這樣做只是將資料從一個位置移到另一個位置)。
藉助 FUSE 透傳功能,負責處理待處理請求的 FUSE 守護程式可以通知 FUSE 驅動程式:可以執行相關操作,並且可將後續所有讀取和寫入請求直接轉發給下層檔案系統。如此一來,就可以避免等待使用者空間 FUSE 守護程式回覆 FUSE 驅動程式請求所產生的額外開銷。
下面對 FUSE 請求與 FUSE 透傳請求進行了對比。
圖 3. FUSE 請求與 FUSE 透傳請求
當應用執行 FUSE 檔案系統訪問時,會發生以下操作:
-
FUSE 驅動程式處理請求並將其加入佇列,然後透過 /dev/fuse 檔案(FUSE 守護程式無法讀取該檔案)中的特定連線例項將請求提交給負責處理該 FUSE 檔案系統的 FUSE 守護程式。
-
當 FUSE 守護程式收到開啟檔案的請求時,它會確定 FUSE 透傳是否適用於該特定檔案。如果適用,守護程式會執行以下操作:
-
將此請求通知 FUSE 驅動程式。
-
使用 FUSE_DEV_IOC_PASSTHROUGH_OPEN ioctl(必須對開啟的 /dev/fuse 的檔案描述符執行)針對該檔案啟用 FUSE 透傳功能。
-
ioctl 接收包含以下內容的資料結構(作為引數)
-
作為透傳功能目標的下層檔案系統檔案的檔案描述符。
-
當前正在處理的 FUSE 請求(必須為待處理狀態或建立並待處理狀態)的唯一識別符號。
-
可以留空並打算供未來實現之用的額外欄位。
-
如果 ioctl 成功,FUSE 守護程式會完成待處理的請求,FUSE 驅動程式會處理 FUSE 守護程式回覆,並且系統會將對下層檔案系統檔案的引用新增到核心中的 FUSE 檔案中。當應用請求對 FUSE 檔案執行讀寫操作時,FUSE 驅動程式會檢查是否有可用的對下層檔案系統檔案的引用。
-
如果有可用的引用,驅動程式會以下層檔案系統檔案為目標,使用相同的引數建立一個新的虛擬檔案系統 (VFS) 請求。
-
如果沒有可用的引用,驅動程式會將請求轉發給 FUSE 守護程式。
對常規檔案執行讀寫操作和讀取迭代器/寫入迭代器操作以及對記憶體對映檔案執行讀寫操作時,都會發生上述操作。針對特定檔案的 FUSE 透傳會一直存在,直到相應檔案關閉為止。
實現 FUSE 透傳功能
如需在搭載 Android 12 的裝置上啟用 FUSE 透傳功能,請將以下程式碼行新增到目標裝置的 $ANDROID_BUILD_TOP/device/…/device.mk
檔案中。
# Use FUSE passthrough
PRODUCT_PRODUCT_PROPERTIES += \
persist.sys.fuse.passthrough.enable=true
如需停用 FUSE 透傳功能,請省略上述配置更改或將 persist.sys.fuse.passthrough.enable 設為 false。如果您之前已啟用 FUSE 透傳功能,停用該功能會使裝置無法使用 FUSE 透傳功能,但裝置仍可正常執行。
注意:請確保實現 FUSE 透傳功能所需的全部核心和 Android 框架元素都存在,否則更改 persist.sys.fuse.passthrough.enable 不會有任何效果。
如需在不刷寫裝置的情況下啟用/停用 FUSE 透傳功能,請使用 ADB 命令更改系統屬性。相關示例如下所示。
adb root
adb shell setprop persist.sys.fuse.passthrough.enable {true,false}
adb reboot
如需獲得其他幫助,請參閱參考實現。
驗證 FUSE 透傳功能
如需驗證 MediaProvider 是否在使用 FUSE 透傳功能,請檢視 logcat
中的除錯訊息。例如:
adb logcat FuseDaemon:V \*:S
--------- beginning of main
03-02 12:09:57.833 3499 3773 I FuseDaemon: Using FUSE passthrough
03-02 12:09:57.833 3499 3773 I FuseDaemon: Starting fuse...
日誌中存在 FuseDaemon: Using FUSE passthrough 條目可確保正在使用 FUSE 透傳功能。
注意:如果 logcat 顯示了與 FUSE 透傳功能相關的錯誤訊息,可能是有部分元件缺失或過時。
Android 12 CTS 包括 CtsStorageTest 測試,內含觸發 FUSE 透傳的測試。如需手動執行該測試,請使用 atest,如下所示:
atest CtsStorageTest
更快地獲得儲存統計資訊
在早期版本的 Android 中,系統會遍歷特定應用擁有的所有檔案以測量磁碟使用情況。此手動測量過程可能需要幾分鐘的計算時間,然後才能在“設定”中向使用者顯示結果。
此外,清除快取資料檔案的內部演算法僅檢視所有應用的修改時間。這使得惡意應用可以透過將修改時間設定在遙遠的未來以使其不當地擁有高於其他應用的優先順序,從而降低整體使用者體驗。
為了提升這些體驗,Android 8.0 會詢問是否利用 ext4 檔案系統的“配額”支援來幾乎即時地返回磁碟使用情況統計資訊。此配額功能還可以防止任何單個應用使用超過 90% 的磁碟空間或 50% 的索引節點,從而提高系統的穩定性。
實現
配額功能是 installd
預設實現的一部分。在特定檔案系統上啟用配額功能後,installd
會自動使用該功能。如果在所測量的塊儲存裝置上未啟用或不支援配額功能,則系統將自動且透明地恢復手動計算方式。
如需在特定塊儲存裝置上啟用配額支援,請執行以下操作:
-
啟用
CONFIG_QUOTA
、CONFIG_QFMT_V2
和CONFIG_QUOTACTL
核心選項。 -
將
quota
選項新增到 fstab 檔案中的 userdata 分割槽:/dev/block/platform/soc/624000.ufshc/by-name/userdata /data
ext4 noatime,nosuid,nodev,barrier=1,noauto_da_alloc
latemount,wait,check,formattable,fileencryption=ice,quota
您可以在現有裝置上安全地啟用或停用 fstab 選項。在更改 fstab 選項後的第一次啟動過程中,fsmgr 會強制執行 fsck 傳遞以更新所有配額資料結構,這可能會導致首次啟動時間稍長。後續啟動不會受到影響。
配額支援僅在 ext4 和 Linux 3.18 或更高版本上進行了測試。如果在其他檔案系統或者較舊的核心版本上啟用,裝置製造商將負責測試和檢查統計資訊的正確性。
不需要特殊硬體支援
驗證
StorageHostTest
下包含 CTS 測試,它們可使用用於測量磁碟使用情況的公共 API。無論是否啟用了配額支援,這些 API 都應返回正確的值。
除錯
測試應用透過為空間大小使用唯一的質數來仔細分配磁碟空間區域。除錯這些測試時,請使用此質數來確定導致任何差異的原因。例如,如果增量為 11MB 的測試失敗,請檢查 Utils.useSpace() 方法以檢視 11MB blob 是否儲存在 getExternalCacheDir() 中。
還有一些可能對除錯有用的內部測試,但它們可能需要停用安全檢查才能透過:
runtest -x frameworks/base/services/tests/servicestests/ \
src/com/android/server/pm/InstallerTest.java
adb shell /data/nativetest64/installd_utils_test/installd_utils_test
adb shell /data/nativetest64/installd_cache_test/installd_cache_test
adb shell /data/nativetest64/installd_service_test/installd_service_test
為什麼要廢棄 SDCardFS?
棄用 SDCardFS 有多方面的原因。
穩定性
SDCardFS 存在與大小寫區分有關的幾個競態條件,以及一些關於記憶體不足情況的問題。在大型目錄中,不區分大小寫的查詢可能會相當慢,因為查詢必須遍歷下層目錄來尋找替代大小寫。同時訪問上層和下層檔案系統也會導致問題。
上游對等性
SDCardFS 需要對 VFS 進行額外的修補,才能支援更改繫結裝載選項。這類修補導致需要執行額外的操作才能接受上游對這些區域所做的更改。SDCardFS 的功能可由上游元件進行復制,從而解決這一難點。
與 API 的功能對等性
在之前的 Android 版本中,分割槽儲存限制了對特定型別後設資料的訪問。透過 SDCardFS 直接訪問儲存空間時,不支援這些分割槽儲存功能。