Android File Reliability: Atomic Writes, Triple Redundancy & Zero-Copy Optimization
This article details Android's file reliability design patterns including atomic write protocols, triple-redundancy fault tolerance, transactional file updates, RAII file descriptor management, zero-copy optimization, dynamic storage trimming strategies, and filesystem-level quota and reservation mechanisms.
Android File Reliability Design Overview
The article systematically covers Android's file reliability design across multiple dimensions: atomic file writes, fault-tolerant multi-copy schemes, transactional file modification, RAII-based file descriptor management, zero-copy file copying, dynamic storage trimming, Linux filesystem quotas, mount reserved space, and filesystem-level integrity mechanisms.
1. AtomicFile — Three-Phase Atomic Write Protocol
Source: frameworks/base/core/java/android/util/AtomicFile.java The core protocol uses a temporary file + fsync + atomic rename:
POSIX rename atomicity: Within the same filesystem, rename is atomic — observers see either the old complete file or the new complete file, never a partial write.
Crash consistency: finishWrite() calls FileUtils.sync(fd) (which invokes fsync(2)) before close() and rename(). If power is lost before rename, only the new data is lost; the original file remains intact.
Backward compatibility: A legacy .bak file is maintained. If .bak exists, startWrite() and openRead() restore from it, allowing old and new strategies to coexist.
No-lock design: Javadoc explicitly states AtomicFile does not provide file-lock semantics; callers must handle mutual exclusion.
2. ResilientAtomicFile — Triple-Copy Fault Tolerance
Source:
frameworks/base/services/core/java/com/android/server/pm/ResilientAtomicFile.javaExtends AtomicFile to solve empty-file, half-written, and primary-copy corruption issues by maintaining three copies: .backup — previous valid version renamed during startWrite() (if last write crashed before finishWrite). baseFile — current official valid file. reserve_copy — extra copy created after successful finishWrite.
openRead recovery order: .backup exists? → restore → baseFile readable? → reserve_copy.
FileIntegrity (fs-verity): Both baseFile and reserve_copy are protected by kernel-level Merkle Tree via FileIntegrity.setUpFsVerity(); any bit-flip causes I/O errors.
3. RestorableFile — Transactional File Modification
Source: frameworks/native/cmds/installd/restorable_file.h Implements a full pre-write backup + transaction commit model in installd. Unlike AtomicFile (which only ensures atomicity of new writes), RestorableFile backs up the existing file before modification, enabling full rollback on commit failure.
4. FD Handle RAII Management — Ownership Model
Core class: unique_fd ( system/libbase/include/android-base/unique_fd.h)
Multiple C++ classes adopt the same RAII pattern to prevent FD leaks:
Move-only, no copy: unique_fd_impl deletes copy assignment, supports move semantics ( &&), ensuring unique ownership like std::unique_ptr.
Policy-based closer: Template <typename Closer> allows different close strategies. Integrated with Bionic's fdsan (file descriptor sanitizer) for tag/close tracking.
Borrowed semantics: borrowed_fd implicitly constructs from unique_fd without taking ownership, analogous to Rust's borrowing.
Compile-time defense: __attribute__((__unavailable__)) on close() and fdopen() catches ownership errors at compile time.
Similar wrappers:
cmd/installd/unique_file.h ScopedFileDescriptor( libs/binder/ndk/include_cpp/android/binder_auto_utils.h)
5. Zero-Copy File Copy Optimization — Fallback Chain
Source: frameworks/base/core/java/android/os/FileUtils.java Implements a descending fallback chain for maximum performance:
sendfile → splice → copy_file_range → manual read/write loopDesign considerations:
Performance first: sendfile / splice avoid user/kernel data copies, yielding 10x+ speedup for GB-sized files.
Auto-fallback: Caller need not know file type; optimal strategy selected automatically.
Cancellable: Every 524 KB checkpoint checks CancellationSignal, enabling mid-copy cancellation.
Progress callbacks: Decoupled via Executor + ProgressListener.
6. Dynamic Storage Trimming
6.1 Size-Based Trimming
DropBoxManagerService.trimToFit()— /data/system/dropbox/, 10 MB or 10% free space. BatteryHistoryDirectory.trim() — /data/system/batterystats/, mMaxHistorySize bytes. IpMemoryStoreService.fullMaintenance() — SQLite DB, 10 MB. AppFunctionPersistentLogger.rotateLogs() — per-file 400 KB, max 4 archives. BootReceiver.addFileWithFootersToDropBox() — system log truncation, 64–192 KB.
6.2 Log Rotation (Size & Time)
SyncLogger.RotatingFileLogger— daily files ( synclog-YYYY-MM-DD.log), 7-day retention. AppFunctionPersistentLogger.rotateLogs() — size rotation: log → log.1 → log.2 … log.N, 400 KB each. Tombstoned (debuggerd) — ring buffer overwrites oldest tombstone. DiskStatsLoggingService — daily full overwrite of diskstats_cache.json (charging + idle).
6.3 Count-Based Trimming
DropBoxManagerService— 1000 entries (300 under low memory). ProcessStatsService — max 8 files. Tombstoned — max_artifacts_ (ring overwrite). SyncLogger — retain 1 file. StackTracesDumpHelper — tombstoned.max_anr_count (default 64). BugreportProgressService — configurable minCount. PeopleService/DataManager — max 30 shortcuts.
6.4 Age-Based Trimming
DropBoxManagerService— 3 days. SyncLogger — 7 days. NotificationHistoryDatabase.prune() — HISTORY_RETENTION_DAYS. PreloadsFileCacheExpirationJobService — ~7 days from boot. InstantAppRegistry.pruneInstantApps() — maxInstalledCacheDuration. EnhancedConfirmationService.pruneOldFinishedCalls() — UNTRUSTED_CALL_STORAGE_TIME_MS. HeapDumpReceiver.cleanupOldFiles() — MIN_KEEP_AGE_MS.
7. Linux Filesystem Quotas (QuotaUtils.cpp)
Source: installd/QuotaUtils.cpp Sets per-UID inode and block hard limits:
// Set inode hard limit (50% of available inodes)
PrepareAppInodeQuota(uid);
// Query usage
GetOccupiedSpaceForUid(uid);Process exceeding quota receives EDQUOT — write fails instead of filling the disk.
8. Mount Reserved Space Mechanism
Ensures system-critical processes retain space after normal processes fill the filesystem. Two implementations:
F2FS: Kernel-native reserve_root= mount option (4K block count). Parsed in system/fs/fs_mgr/libfstab/fstab.cpp:
if (entry->fs_type == "f2fs" && StartsWith(flag, "reserve_root=")) {
entry->reserved_size = size_in_4k_blocks << 12; // convert to bytes
}ext4: reservedsize= in fstab (byte count), applied via fs_mgr + tune2fs to superblock reserved blocks.
... /data ext4 ... latemount,wait,check,reservedsize=32MiB9. Filesystem-Level Guarantees
fsync— data flush to storage semantics. rename — atomicity implementation. quotactl — quota enforcement mechanism. FileIntegrity — fs-verity Merkle tree protection.
By studying these battle-tested designs, developers can build more reliable file-handling logic in their own Android software.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Thought Artisan
I think, therefore I am; recording insights from daily life and technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
