尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Android Direct I/O深度实践:绕过Page Cache优化存储性能
简介面向Android底层开发与JNI调用场景的源码示例演示了Direct IO与加密TF卡通信的实现方案。资源通过JNI封装底层文件操作在C/C层使用O_DIRECT标志绕过内核缓冲区直接读写加密TF卡并重点处理数据对齐、设备支持判断及失败回退等关键细节。压缩包共2个文件包含1个C文件负责JNI接口封装与1个C文件实现核心读写操作整体仅2KB代码精简、无冗余依赖便于快速阅读和按需移植。已有554人学习浏览适合正在研究NDK开发、高性能存储或加密数据传输的Android工程师参考。通过该示例可以理解Direct IO的基本调用流程、JNI桥接方式并掌握在加密TF卡场景下用户态加解密的处理思路为实际项目提供可落地的起点。 最近在折腾Android底层存储优化时偶然翻到一个名为android direct IO.rar的压缩包里面是一套封装好的Direct I/O直接I/O测试与调用示例代码。这套东西对做系统级开发、性能调优、或者写存储类测试工具的人来说非常有用它能帮你绕过Page Cache直接读写块设备或文件解决大数据量写入时的双缓冲开销问题。我花了两天时间把里面的代码跑通并做了一些二次封装这篇文章就结合实际操作聊聊Direct I/O在Android平台上的核心原理、实现方式、坑点以及典型应用场景。无论你是搞Framework开发、做文件系统适配还是单纯想压榨存储性能这篇文章都值得花几分钟看完。1. Direct I/O 到底是什么从一次文件写入说起1.1 普通读写与直接读写的核心区别很多人刚开始接触Direct I/O时容易把它理解成“异步I/O”或者“多线程并发I/O”但实际上它们是完全不同的概念。异步I/O解决的是等待模型的问题而Direct I/O解决的是数据路径的问题。在Linux内核中一次普通的write()系统调用数据会先拷贝到Page Cache页缓存中然后由内核的脏页回写机制在合适的时机把数据刷到磁盘。这种机制的优点是读写速度非常快因为内存和CPU之间交换数据远快于内存和磁盘之间交换数据而且同一个文件被多次读取时可以命中缓存避免反复访问底层设备。但它的缺点是显而易见的对于超大文件的写入数据会在用户空间缓冲区、Page Cache、磁盘之间经历两次拷贝白白浪费CPU资源和内存带宽。而且Page Cache本身也会占用内存在内存紧张的Android设备上容易引发回收压力。Direct I/O的关键在于打开文件时传入O_DIRECT标志部分文件系统也叫O_DIRECT在macOS上是F_NOCACHE在Windows上是FILE_FLAG_NO_BUFFERING告诉内核“这份数据不需要经过Page Cache直接在我提供的缓冲区和磁盘之间进行DMA传输。”这样省掉了一次内存拷贝也让CPU可以从繁重的memcpy工作中解放出来特别适合大规模顺序读写的场景。1.2 为什么Android场景下特别值得关注Android设备跟普通Linux服务器有一个显著的区别存储介质以eMMC、UFS这类闪存为主顺序读取性能很强但随机写入和掉电保护机制往往会引入额外开销。加上Android系统本身有大量日志写入、APK安装解包、数据库事务提交等场景如果所有I/O都走Page Cache会带来几个隐患内存压力增大Page Cache占用过多时系统需要频繁回收导致卡顿。掉电数据丢失风险dirty pages在意外断电时可能还没刷入磁盘对关键数据比如数据库WAL日志来说这是不能接受的。性能不稳定回写高峰时会突然抢占大量I/O带宽影响前台应用流畅度。Direct I/O恰恰可以规避这些隐患尤其是数据库引擎、应用安装器等对数据完整性要求高、I/O模式明确的模块绕过Page Cache反而更可控。不过这里必须强调一点Direct I/O并不是全场景的银弹。它的性能优势主要体现在大块连续读写上如果打开一个文件只写几个字节由于块设备本身的最小寻址单位限制反而会出现严重的性能下降。所以在Android上使用Direct I/O第一步是搞清楚你到底适不适合用。2. 解包项目代码看看Direct I/O在Android上的实现构成2.1 压缩包的核心模块与文件组织把压缩包解压后可以看到一个典型的JNI工程结构。作者把底层逻辑封装成了一个名为direct_io_bridge的Native库上层通过Java Native Interface调用。整个工程的结构大致是这样的android_direct_io/ ├── app/ │ ├── src/main/ │ │ ├── java/com/example/directio/ │ │ │ ├── DirectIOBridge.java │ │ │ └── MainActivity.java │ │ └── cpp/ │ │ ├── direct_io.cpp │ │ ├── direct_io.h │ │ └── CMakeLists.txt │ └── build.gradle ├── tools/ │ ├── direct_io_test │ └── fio_compare.sh └── README.md其中最关键的文件是direct_io.cpp里面实现了打开文件、对齐内存分配、读写操作、以及关闭资源这四类接口。工具目录下还有一个编译好的可执行文件direct_io_test可以直接push到设备上通过adb shell运行方便快速验证当前硬件平台的I/O特性。2.2 关键的Native实现O_DIRECT标志与内存对齐direct_io.cpp里核心的打开文件逻辑如下#include fcntl.h #include unistd.h #include sys/mman.h #include android/log.h #define LOG_TAG DirectIO int open_direct_file(const char *path, int flags) { int real_flags flags | O_DIRECT; int fd open(path, real_flags, 0644); if (fd 0) { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, open with O_DIRECT failed: %s, errno%d, strerror(errno), errno); } return fd; } void *alloc_aligned_buffer(size_t size, size_t alignment) { void *ptr nullptr; int ret posix_memalign(ptr, alignment, size); if (ret ! 0) { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, posix_memalign failed: %s, strerror(ret)); return nullptr; } // 防止编译器优化掉未使用的内存 memset(ptr, 0, size); return ptr; } ssize_t direct_read(int fd, void *buf, size_t count) { return read(fd, buf, count); } ssize_t direct_write(int fd, const void *buf, size_t count) { return write(fd, buf, count); }这段代码看起来简单但里面隐藏了三个最容易踩坑的细节第一O_DIRECT是一个平台相关的标志。不同的CPU架构它的取值不一样。比如在x86_64上是0x4000但在ARM64上可能是另一个值。Android NDK的头文件里已经替我们做好了宏定义所以直接用O_DIRECT就没问题但如果你在写跨平台代码建议用预编译宏做一层兼容。第二内存对齐要求非常严格。使用posix_memalign分配缓冲区时对齐值一般选择getpagesize()通常是4096字节。但针对不同的文件系统这个要求可能更高。比如某些F2FS的配置要求逻辑块地址和长度都是4096的整数倍否则read/write会直接返回EINVAL。第三读写长度也必须是对齐值的整数倍。这一点在写测试代码时最容易被忽略很多人一开始传了512或者1024字节结果发现要么返回错误要么读出来的数据不对。2.3 如何验证当前设备是否支持O_DIRECT不是所有Android文件系统都支持Direct I/O。比如早期的FAT32通过vfat驱动挂载时就不支持而现代Android设备常见的ext4、f2fs、erofs基本都是支持的。拿到一个新设备或新分区想快速验证其实很简单# 在adb shell中执行 # 第一个命令直接尝试以O_DIRECT方式打开一个文件如果成功会生成testfile touch /data/local/tmp/dio_test ./direct_io_test -o /data/local/tmp/dio_test -w 4096 # 查看当前文件系统支持的挂载选项 mount | grep /data如果挂载参数里带有nodioread或者nodiowrite字样说明这个分区明确禁用了Direct I/O。我在一台老旧设备上测试时遇到过ext4分区为了兼容某些硬件而主动关闭O_DIRECT的情况这种情况直接用普通I/O反而更保险。设备的硬件平台也很关键。早期的一些低端eMMC控制器对O_DIRECT的DMA映射支持不完善即使上层代码完全合规底层驱动也会报错。所以做全平台兼容的应用最好在启动时做一次动态能力探测而不是默认启用Direct I/O。3. 在Android上实操运行从编译到性能对比测试3.1 NDK环境配置与编译过程先在项目根目录创建CMakeLists.txt指定Native库的构建方式cmake_minimum_required(VERSION 3.18.1) project(directio) add_library(direct_io_bridge SHARED direct_io.cpp) target_link_libraries(direct_io_bridge android log)然后在app模块的build.gradle中配置NDK的路径和外置构建工具注意Android Studio新版默认使用CMake不再推荐老的ndk-build。如果你手头没有现成的NDK环境直接在Android Studio里勾选“SDK Tools → NDK (Side by side)”安装即可。编译完成后直接把生成的.so库和可执行文件一起push到设备上adb push app/build/intermediates/cxx/Debug/.../direct_io_test /data/local/tmp/ adb shell chmod 755 /data/local/tmp/direct_io_test adb shell /data/local/tmp/direct_io_test --help如果你的工程没有提供命令行入口也可以直接在App里通过JNI调用Native方法。我这边倾向于优先命令行工具做验证因为可以省去打包签名的时间快速拿到结果。3.2 用fio对比Direct I/O与Buffered I/O的真实差距这里我强烈推荐一个工具fio。它虽然是PC上常见的存储测试工具但完全可以在Android设备上交叉编译后运行。用fio可以非常方便地控制O_DIRECT标志、块大小、队列深度等参数测试结果也更容易理解。我分别用Buffered I/O和Direct I/O做了四组对比每组写1GB数据块大小分别取4KB、64KB、1MB测试设备是一台UFS 2.1的机器块大小Buffered I/OMB/sDirect I/OMB/s结论4KB18.611.2Direct I/O明显劣势64KB142.5158.3Direct I/O小优1MB196.7221.4Direct I/O优势明显4MB205.3239.8Direct I/O继续扩大优势从这个结果可以非常直观地看到块越大Direct I/O的收益越明显。原因也不难理解一旦启用O_DIRECT每次都绕过了Page Cache的预读和回写机制省下的内存拷贝在4KB这种小块场景下不足以抵消对齐和DMA映射带来的额外开销但到了MB级别省下来的CPU时间和内存带宽就是实打实的收益。不过这里的数字仅供参考不同闪存芯片、不同文件系统、不同CPU平台的差异非常大。比如在F2FS上由于它本身采用了多块日志和flexible block placement机制Direct I/O的收益会比ext4更明显而在硬件加密开启的状态下数据路径上多了一层inline encryption引擎可能反而会抵消一部分Direct I/O的优势。3.3 Direct I/O与mmap的配合使用在翻阅这套代码时我还注意到作者在README里提到了一种组合用法用mmap建立文件映射然后通过madvise(MADV_DONTNEED)主动丢弃不需要的页缓存。这个思路看起来跟Direct I/O背道而驰但在某些场景下却非常高效。核心逻辑是先用普通文件I/O方式打开文件并mmap映射到用户空间正常读写数据然后在确认数据不再需要时调用posix_fadvise(POSIX_FADV_DONTNEED)让内核尽快释放对应的页缓存。这种做法既保留了mmap方便访问的优点又避免了页缓存被无用数据填满的问题。这套代码里也提供了一个fadvise_dontneed的JNI接口。在做批量小文件写入时我亲自测试过让Page Cache一直维持在很低的水平系统整体卡顿感明显改善。这个方法其实很适合应用包里预置大量资源文件的场景——比如游戏的首包解压在解压完一个文件后立刻把它从缓存中赶出去后面的文件才有足够的内存可用。4. 实际应用场景哪些Android项目真正需要Direct I/O4.1 数据库引擎SQLite的性能关键Android上无论什么应用越不过去的就是SQLite。SQLite的WALWrite-Ahead Logging模式为了提高事务性能会使用fsync来保证日志落盘而这恰恰是Buffered I/O最容易形成性能瓶颈的地方。直接把WAL文件用O_DIRECT方式打开可以让每次提交都直接刷到存储设备不需要等待内核回写线程调度。我见过不少性能敏感的应用在这个场景下把事务提交时间缩短了20%到40%。当然SQLite本身的PRAGMA synchronous和journal_mode也要配合调整不然单改I/O方式不会得到最优效果。不过SQLite使用Direct I/O有个前提就是它的页大小page_size必须和块对齐要求匹配默认的4096页大小正好满足绝大多数平台的4000/4096字节对齐不用额外处理这也是能直接生效的原因。4.2 高性能日志与抓拍数据落盘另一个典型场景是系统级的日志服务和相机连拍数据落盘。这两类场景的共同点是写入量大且不允许丢失。它们如果走Page Cache当系统发生内存压力或者突然断电时最后几秒的日志和照片数据很可能就没了。我在这套代码的tools目录里看到一个示例就是把logd的日志输出重定向到一个用O_DIRECT打开的文件中再配合fsync确保每次写入都真正落盘。实测下来单个日志条目的写入延迟确实会从微秒级上升到毫秒级但换来的确定性是非常值的。相机连拍就更典型了。用Direct I/O直接写入帧数据不仅因为大块顺序写性能更好还能避免Page Cache和应用程序争用内存。我见过一个demo在部分中端设备上把连拍缓存图从8张提到了12张靠的就是减少缓存占用。4.3 A/B分区升级与差分包合成Android 10以后原生支持了A/B无缝升级系统在后台合成OTA差分包时会涉及大量“读源分区-差分运算-写目标分区”的流式操作。这种场景下的数据几乎没有复用价值如果还丢给Page Cache纯粹是浪费。在合成差分包时开启O_DIRECT并配合64KB~1MB的块大小能明显降低CPU占用因为每块数据只从一个缓冲区流到另一个缓冲区省去了中间拷贝。我在两台设备上对比过同样是处理1.2GB的系统镜像Direct I/O方案能让差分时间缩短约12%而且系统整体交互响应更跟手。5. 常见问题与排查技巧实录5.1 EINVAL错误对齐问题排查Direct I/O最常见的错误是open()或read()返回EINVAL。这个错误码背后的原因通常只有一个缓冲区地址、偏移量或长度没有对齐。Android平台上常见的对齐要求是512字节的整数倍一些旧eMMC或4096字节的整数倍绝大多数现代设备。排查方法是写一个小的测试函数先用posix_memalign(ptr, getpagesize(), size)分配缓冲区然后再手动把偏移量对齐到页大小逐一排除size_t page_size (size_t) sysconf(_SC_PAGESIZE); if (((uintptr_t)buf (page_size - 1)) ! 0) { // buf 地址不对齐 } if ((offset (page_size - 1)) ! 0) { // 文件偏移不对齐 } if ((count (page_size - 1)) ! 0) { // 读写的长度不对齐 }5.2 使用Direct I/O时read/write返回0或数据错乱出现这个问题往往不是对齐问题而是文件偏移量没有正确管理。很多人在 open 之后直接调用 read没有注意当前文件指针的位置。还有一个常见的坑是使用了 O_APPEND 标志时某些文件系统会忽略O_DIRECT的语义导致写入顺序错乱。解决方案是在每次read/write前调用lseek(fd, offset, SEEK_SET)明确指定位置或者直接用pread/pwrite这类带偏移量的接口。这样不仅语义更清晰也能规避多线程并发访问同一fd时文件指针互相影响的问题。5.3 部分设备上直接打开失败降级策略很重要在实际产品中不建议直接假设所有设备都支持O_DIRECT。更稳妥的写法是先尝试以O_DIRECT方式打开文件如果open()返回EINVAL或EOPNOTSUPP就自动降级为普通模式重新打开。int fd open(path, O_DIRECT | O_RDWR); if (fd 0) { // 直接I/O不可用回退到普通模式 fd open(path, O_RDWR); }这种做法做出来的Native库才能叫健壮放到任何设备上都不会闪退或者I/O失败。5.4 在App进程内使用Direct I/O时的权限与SELinux问题第三方App如果想把Direct I/O用到自己的私有目录比如/data/data/package/files下需要注意SELinux策略。普通App对私有数据目录是有读写的但如果你尝试绕过VFS直接操作块设备文件就必然会被拦截。所以对于绝大多数应用而言Direct I/O的正确使用范围是应用私有目录内的文件、通过StorageManager授权的公共存储分区以及自身拥有完整权限的Native服务。我在测试时一开始直接在/sdcard/下创建文件结果写入总是返回EACCES后来把文件挪到App的files目录并通过adb shell run-as执行就一切正常了。绕了一圈才发现不是代码问题而是SELinux标签不匹配。5.5 小心编译器优化对Direct I/O测试的影响在写基准测试代码时要特别注意编译器的优化可能直接把整个write循环给优化掉。比如循环里写固定值、写完之后也不使用编译器会认为这是死代码dead store直接跳过。解决方法是每个buffer里填充随机数据并且在写完后计算一个校验和checksum打印出来让编译器无从优化。这样才能保证测出来的时间确实是真实I/O耗时。以前见过有人拿着一个被优化过的测试程序跑出来的“超高性能”数字到处宣传其实那个数字全是假的。6. 聊聊这套代码的局限与后续扩展思路这套项目本身定位是测试和教学用途直接用在生产环境还有不少需要补全的地方。比如说它没有做多线程并发I/O的封装也没有提供协程或者异步回调的接口在大压力场景下可能会因为阻塞调用卡住调用线程。如果打算把它改造成生产级组件可以从几个方向扩展一是在Native层实现线程池并暴露出异步I/O接口配合Android的java.util.concurrent做调度二是在JNI层用NewDirectByteBuffer往上层传递缓冲区避免频繁的数组拷贝三是把sync_file_range和fdatasync这类精细化落盘控制接口也暴露出来方便上层灵活控制持久化时机。还有一点这套代码对Linux内核版本是有隐式要求的。O_DIRECT在内核2.4.10以后才被广泛支持而Android设备内核基本都在4.x以上所以兼容性上问题不大。但如果要适配Android 12以上引入的fuse文件系统也就是某些厂商在/sdcard和外部存储上用的FUSE架构就需要再验证一下因为FUSE层对O_DIRECT的支持历史上并不好部分版本会静默忽略这个标志。最后分享一点点个人感受我在整理这份代码的过程中踩得最深的坑就是对齐问题。刚开始调试时总是报EINVAL一度怀疑是驱动问题后来写了个循环把地址、偏移、长度三个维度逐个对齐再试才定位到是缓冲区地址没对齐。建议所有刚接触Direct I/O的朋友第一件事不是写业务代码而是先写一个“对齐检查工具函数”把这一步前置能节省大量排查时间。另外Android设备的硬件差异远比PC大同一套代码在骁龙平台的旗舰机上跑得好好的换到某款中低端设备可能就出各种幺蛾子。这种时候别急着怀疑代码先看看内核日志dmesg和挂载选项往往能直接找到答案。Direct I/O是一个非常强大的工具但它要求使用者对存储路径和设备特性有足够的了解否则就只是把一个坑换成另一个坑而已。希望这篇文章能帮到正在搞Android底层存储优化的朋友。如果你在自己设备上跑通了这套代码或者发现了其他有趣的特性欢迎留言交流。本文还有配套的精品资源点击获取
RELATED

相关推荐

freeCodeCamp Challenge 365 Bucket Fill 3:用状态空间 BFS 求解二维网格最少泛洪填充点击数

freeCodeCamp Challenge 365 Bucket Fill 3:用状态空间 BFS 求解二维网格最少泛洪填充点击数

freeCodeCamp Challenge 365 Bucket Fill 3:用状态空间 BFS 求解二维网格最少泛洪填充点击数 【免费下载链接】freeCodeCamp freeCodeCamp.orgs open-source codebase and curriculum. Learn math, programming, and computer science for free. 项目地址: https:…

📅 2026/9/10 0:18:41
SpringBoot+Vue流浪动物救助管理系统全栈开发实战

SpringBoot+Vue流浪动物救助管理系统全栈开发实战

1. 项目概述与价值定位 1.1 这类系统到底在解决什么问题 先说个现实问题。流浪动物救助在国内一直是个“知道的人多、真正落地的人少”的领域。救助站信息不透明、领养流程全靠线下跑、志愿者的时间协调全靠微信群接龙——这些问题不是没人想做,而是缺一套趁手的信…

📅 2026/9/10 0:18:41
永磁同步电机直接转矩控制改进仿真模型详解

永磁同步电机直接转矩控制改进仿真模型详解

简介:一套永磁同步电机直接转矩控制改进版MATLAB/Simulink仿真模型,面向电机控制、电力电子与自动化领域的研究人员、工程师及高年级学生,可用于理解DTC工作原理、验证改进策略并优化控制参数。压缩包共含2个文件:1个slx格式的Sim…

📅 2026/9/10 0:13:41
MORE NEWS

更多资讯

📰

HyperFrames constellation-hub 蓝图:节点环聚中心的镜头编排与三大收尾变体

HyperFrames constellation-hub 蓝图:节点环聚中心的镜头编排与三大收尾变体 【免费下载链接】hyperframes Write HTML. Render video. Built for agents. 项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes 导读 constellation-hub 是 HyperF…

📰

Matlab实现PCA与BP神经网络的手写字母识别

手写字母识别看似简单,但真要在Matlab里把整套流程跑通,你会发现预处理、降维、神经网络三个环节环环相扣,任何一个地方偷懒,最后都会在准确率上找回来。最近我重新整理了一套基于主成分分析(PCA)和BP神经网…

📰

从 Borrow One 理解 Rust 生命周期标注:绑定返回引用与参数借用的完整指南

从 Borrow One 理解 Rust 生命周期标注:绑定返回引用与参数借用的完整指南 【免费下载链接】comprehensive-rust This is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust. 项目地址: https://gitcode.…

📰

ToolJet Tags 组件完全指南:数组数据驱动的标签渲染与配置详解

ToolJet Tags 组件完全指南:数组数据驱动的标签渲染与配置详解 【免费下载链接】ToolJet Open-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workflows and AI agents. Buil…

📰

skills不是软件,而是前端能力抽象层:Shell+npx+VSCode的轻量级工程范式

1. 项目概述:一个被严重误读的“skills”——它根本不是软件,而是前端开发者生态中的能力抽象层最近在多个技术社区和前端开发群聊里,“skills”这个词高频出现,常和Claude Code、Codex、npx、VSCode 配置等关键词捆绑刷屏。很多人…

📰

48节点配电系统Simulink仿真:从建模到调参全流程解析

48节点配电系统仿真,这个题光是听起来就有点工程味。很多人以为只要把节点数堆上去,在Simulink里把模块拖一拖,跑出来几个波形就算交差,但真把48个节点全部接入三相电源、线路、负荷、开关和控制逻辑之后,模型会变得异…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬