小明的玩物志
首页
搜索
归档
白色风车
文章
17
分类
4
标签
8
归档
搜索
分类
标签
技术分享
🗒️基于xhook的文件IO监控
发布于: 2021-7-2
最后更新: 2024-8-21
次查看
Android
开发
type
status
slug
date
summary
tags
category
password
icon

1、概述

I/O是Linux内核里面除了进程管理、内存管理之外另一个比较重要的概念。I/O涉及的知识覆盖面会非常宽泛,从用户态编程框架模型、到内核系统调用、文件系统、I/O调度、磁盘驱动、网络驱动等都有涉及。I/O对于系统的影响,其实更多的体现在性能方面。
今天我们将先宏观了解下I/O处理的整个流程,随后对I/O各环节的监控手段进行认识,最后分享下应用层开发中对文件的监控原理与实现以及一些优化建议。

2、I/O基础


2.1 I/O的分类

2.1.1 标准I/O - Buffered I/O
I/O 操作由应用程序、文件系统和磁盘共同完成,应用程序将 I/O 命令发送给文件系统,文件系统在合适的时间把 I/O 指令发送给磁盘。移动设备大部分情况都是标准I/O ,流程如下图:
暂时无法在文档外展示此内容
notion image
  1. 系统调用read会触发相应的VFS(Virtual Filesystem Switch)函数,传递的参数有文件描述符和文件偏移量。
  1. VFS确定请求的数据是否已经在内存缓冲区中;若数据不在内存中,确定如何执行读操作。
  1. 假设内核必须从块设备上读取数据,这样内核就必须确定数据在物理设备上的位置。 这由映射层(Mapping Layer)来完成。
  1. 此时内核通过通用块设备层(Generic Block Layer)在块设备上执行读操作,启动I/O 操作,传输请求的数据。
  1. 在通用块设备层之下是I/O调度层(I/O Scheduler Layer),根据内核的调度策略,对等待的I/O等待队列排序。
  1. 最后,块设备驱动(Block Device Driver)通过向磁盘控制器发送相应的命令,执行真正的数据传输。
这是一套标准的IO流程(Buffered IO)
在 Linux 的缓存 I/O 机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的用户空间。
优点:
  • 减少硬盘读写次数,提高性能;
  • 在一定程度分离内核空间和用户空间,保护系统安全。
缺点:
  • 数据会在用户空间和内核空间之间来回地拷贝,这会增大CPU及内存的开销。
2.1.2 直接I/O - Direct IO
应用程序跳过内核空间缓冲区直接访问磁盘,这可以克服缓存I/O的缺点,但应用程序一般需要自己实现缓存。比如数据库程序可能更倾向于自己实现缓存机制。
当打开文件不指定 O_DIRECT 标志时,那么就默认使用 缓存I/O 方式打开。我们可以通过下图来了解 缓存I/O 处于文件系统的什么位置:
notion image
优点:
减少一次内核缓冲区到用户程序缓存的数据复制,减小了CPU和内存的开销。
缺点:
访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘加载,需要在应用侧做好缓存管理,对编码要求高。
应用场景:
数据库系统,其高速缓存和IO优化机制均自成一体,无需内核消耗CPU时间和内存去完成相同的任务。
2.1.3 内存映射 - mmap
mmap操作提供了一种机制,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。Android的跨进程机制,Binder.c驱动中使用内存映射来实现。
notion image
优点
  • 减少系统调用。我们只需要一次 mmap() 系统调用,后续所有的调用像操作内存一样,而不会出现大量的 read/write 系统调用。
  • 减少数据拷贝。普通的 read() 调用,数据需要经过两次拷贝;而 mmap 只需要从磁盘拷贝一次就可以了,并且由于做过内存映射,也不需要再拷贝回用户空间。
  • 可靠性高。mmap 把数据写入页缓存后,跟缓存 I/O 的延迟写机制一样,可以依靠内核线程定期写回磁盘。但是需要提的是,mmap 在内核崩溃、突然断电的情况下也一样有可能引起内容丢失,当然我们也可以使用 msync 来强制同步写。
缺点
  • 虚拟内存增大。电视目前还都是32位系统,虚拟内存有限,如果映射太大文件,容易出现虚拟内存不足,导致OOM。

2.2 文件系统

文件系统是操作系统用于明确存储设备(常见的是磁盘,也有基于NAND Flash的固态硬盘)或分区上的文件的方法和数据结构;即在存储设备上组织文件的方法。
Linux以文件的形式对计算机中的数据和硬件资源进行管理,也就是彻底的一切皆文件,反映在Linux的文件类型上就是:普通文件、目录文件(也就是文件夹)、设备文件、链接文件、管道文件、套接字文件(数据通信的接口)等等。
VFS,Virtual File System虚拟文件系统,也称为虚拟文件系统开关(Virtual Filesystem Switch),就是采用标准的Linux系统调用读写位于不同物理介质上的不同文件系统,即为各类文件系统提供了一个统一的操作界面和应用编程接口,VFS是一个内核软件层,换句话说就是向上提供了统一的文件访问接口,而向下则兼容了各种不不同类型的文件系统。
notion image
2.2.1 Android支持的文件系统
每个文件系统都有适合自己的应用场景
EROFS全称为Enhanced Read-Only File System(可扩展的只读文件系统),是在 Linux 4.19 中引入的只读文件系统。它支持压缩和去重,并针对读取性能进行了优化。
EROFS 与其他压缩文件系统之间的主要区别在于,它支持就地解压缩。压缩的数据存储在块末尾,以便能够解压缩到同一页面中。在 EROFS 映像中,超过 99% 的块能够使用此方案,因此无需在读取操作期间分配额外的页面。
EROFS 图片不必压缩。但是,使用压缩功能时,图片的大小平均缩小约 25%。在最高压缩级别下,图片可缩小多达 45%。
EROFS 在Android13中完全使用
无法复制加载中的内容
文件系统是建立在物理存储上的一套存储和检索系统,所以在同一物理设备的基础上,文件系统的设计好坏也会明显影响整个文件读取和写入的性能表现。
电视的文件系统
xxxx:/ # df Filesystem 1K-blocks Used Available Use% Mounted on tmpfs 942908 652 942256 1% /dev tmpfs 942908 0 942908 0% /mnt tmpfs 942908 0 942908 0% /apex /dev/block/dm-0 1231512 1182804 48708 97% / /dev/block/dm-1 826976 775776 51200 94% /vendor tmpfs 942908 204 942704 1% /mnt/vendor/tmp /dev/block/mmcblk0p31 26688768 2691964 23996804 11% /data /dev/block/mmcblk0p26 91888 68 91820 1% /cache /dev/block/mmcblk0p27 126912 101004 25908 80% /mnt/vendor/tvservice /dev/block/mmcblk0p8 124908 87984 36924 71% /vendor/tvconfig /dev/block/mmcblk0p6 60400 356 60044 1% /vendor/tvcertificate /dev/block/mmcblk0p28 928 72 856 8% /factory /dev/block/mmcblk0p21 5760 5760 0 100% /mnt/vendor/3rd /dev/block/dm-2 466044 412 465632 1% /mnt/scratch overlay 466044 412 465632 1% /mnt/vendor/linux_rootfs /dev/block/mmcblk0p22 26128 156 25972 1% /data/vendor/3rd_rw /data/media 26688768 2691964 23996804 11% /mnt/runtime/default/emulated
notion image

2.3 磁盘

磁盘指的是系统的存储设备,常见的有机械硬盘、固态硬盘等。
电视都是使用eMMC,特点就是功耗低,容量小,随机读写性能差。
如果发现应用程序要读的数据没有在页缓存中,这时候就需要真正向磁盘发起 I/O 请求。磁盘 I/O 的过程要先经过内核的通用块层->I/O 调度层->设备驱动层->最后才会交给具体的硬件设备处理。
notion image
  • 通用块层。接收上层发出的磁盘请求,并最终发出 I/O 请求。它与 VPS 的作用类似。
  • I/O 调度层。根据设置的调度算法对请求合并和排序。不能接收到磁盘请求就立刻交给驱动层处理。
  • 块设备驱动层。根据具体的物理设备,选择对应的驱动程序,通过操控硬件设备完成最终的 I/O 请求。
notion image
notion image

3、性能分析

正如下图你所看到的,整个 I/O 的流程涉及的链路非常长。我们在应用程序中通过打点,发现一个文件读取需要 300ms。但是下面每一层可能都有自己的策略和调度算法,因此很难真正的得到每一层的耗时。
硬件
eMMC UFS 不同协议的差别,设备老化,碎片化,可用空间等
notion image
notion image
块设备层
因为块设备层的耗时有两方面的耗时组成,一个是submit_bio的耗时,一个是请求排队耗时。
文件系统层
可以得到文件系统层总共发送了多少fsyncor writeor read系统调用数目。
推荐Brendan Gregg的内核性能相关文章,分为性能观测,评测,调优等工具。
个人常用的工具:
  1. lsof -p PID打印进程所打开的文件。
  1. strace -p PID 打印进程的系统调用。
系统调用write频繁,看日志可知是log频繁打印。
writev(3, [{iov_base="\0MI\0032sc\265W\22\32", iov_len=11}, {iov_base="\4", iov_len=1}, {iov_base="System.out\0", iov_len=11}, {iov_base="{ when=-18ms barrier=571089 }\0", iov_len=30}], 4) = 53futex(0x81885768, FUTEX_REQUEUE_PRIVATE, 0, 1, 0x8188574c) = 0futex(0x8188574c, FUTEX_WAKE_PRIVATE, 1) = 0clock_gettime(CLOCK_REALTIME, {tv_sec=1668493827, tv_nsec=438013217}) = 0getuid32() = 1000writev(3, [{iov_base="\0MI\0032sc!\215\33\32", iov_len=11}, {iov_base="\3", iov_len=1}, {iov_base="Choreographer\0", iov_len=14}, {iov_base="doFrame start mLock (long frameT"..., iov_len=77}], 4) = 103clock_gettime(CLOCK_REALTIME, {tv_sec=1668493827, tv_nsec=438559717}) = 0getuid32() = 1000writev(3, [{iov_base="\0MI\0032sc\345\343#\32", iov_len=11}, {iov_base="\3", iov_len=1}, {iov_base="Choreographer\0", iov_len=14}, {iov_base="doFrame enter mLock (long frameT"..., iov_len=77}], 4) = 103----------------------------------------------------------- logcat: D/Choreographer: doFrame start mLock (long frameTimeNanos: 104689754032995, int frame:808435) D/Choreographer: doFrame enter mLock (long frameTimeNanos: 104689754032995, int frame:808435) D/Choreographer: RunCallback: type=0, action=android.view.ViewRootImpl$ConsumeBatchedInputRunnable@16fdf13, token=null, latencyMillis=8 D/Choreographer: RunCallback: type=3, action=android.view.ViewRootImpl$TraversalRunnable@e1c1a02, token=null, latencyMillis=8
  1. BCC (Android12 GKI)
扩展型柏克莱封包过滤器 (eBPF) 是一个内核中的虚拟机,可运行用户提供的 eBPF 程序来扩展内核功能。这些程序可以挂接到内核中的探测点或事件,并用于收集有用的内核统计信息、监控和调试。
notion image

4、文件的监控

内核中提供的工具可以反映整个系统的IO状况,但是有以下2个缺点:
1 距离业务层比较遥远,跟代码中的read,write不对应(由于系统预读 + pagecache + IO调度算法等因素, 也很难对应);
2 是系统级的,我们只想关系应用层的问题。
实际应用开发中,需要快速知道哪些I/O操作不合理,并且发现代码中不合理的I/O操作的代码,今天我们一起来看下这些问题如何解决。
出于对性能,Native代码监控以及Android平台兼容性等一些考虑,经过调研发现采用Native Hook的方案能够满足我们的需求。
Native Hook具体使用PLT Hook方案,在天眼中有多个性能检测工具使用此方案来实现。
电视目前系统都是32位,若为64位还需兼容新的接口。
目标函数:
int open(const char *pathname, int flags, mode_t mode); //返回值fd ssize_t read(int fd, void *buf, size_t size); ssize_t write(int fd, const void *buf, size_t size); int close(int fd);
如何找到是在哪个库呢?根据FileInputStream打开文件来追踪调用栈。
java : FileInputStream -> IoBridge.open -> Libcore.os.open -> BlockGuardOs.open -> Posix.open ↓ jni : libcore_io_Posix.cpp static jobject Posix_open(...) { ... int fd = throwIfMinusOne(env, "open", TEMP_FAILURE_RETRY(open(path.c_str(), flags, mode))); ... } ------------------------------ Android 9 java : FileInputStream -> IoBridge.open -> Libcore.os.open-> -> BlockGuardOs.open -> Linux.open ↓ jni: libcore/luni/src/main/native/libcore_io_Linux.cppstatic jobject Linux_open(JNIEnv* env, jobject, jstring javaPath, jint flags, jint mode) { ScopedUtfChars path(env, javaPath); if (path.c_str() == NULL) { return NULL; } int fd = throwIfMinusOne(env, "open", TEMP_FAILURE_RETRY(open(path.c_str(), flags, mode))); return createFileDescriptorIfOpen(env, fd); }
在NativeCode.bp编译脚本可知,编译成libjavacore.so
cc_library_shared { name: "libjavacore", visibility: [ "//art/build/apex", "//art/runtime", ], apex_available: [ "com.android.art", "com.android.art.debug", ], defaults: [ "core_native_default_flags", "core_native_default_libs", ], srcs: [ ":luni_native_srcs", ], ... }
于是只要Hooklibjavacore.so的read符号就可以了。不同版本系统调用也有差异,注意适配性。
核心代码:
//找到libjavacore库,将其read方法替换 void *soinfo = xhook_elf_open("libjavacore.so"); if (xhook_got_hook_symbol(soinfo, "read", (void *) ProxyRead, (void **) &original_read) != 0) { LOGW("doHook hook read failed, try __read_chk"); if (xhook_got_hook_symbol(soinfo, "__read_chk", (void *) ProxyReadChk, (void **) &original_read_chk) != 0) { LOGE("doHook hook failed: __read_chk"); xhook_elf_close(soinfo); return JNI_FALSE; } } //Hook读方法 ssize_t ProxyRead(int fd, void *buf, size_t size) { std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now(); ssize_t rSize = original_read(fd, buf, size); std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); int cost = std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count(); LOGE("Proxy Read fd:%d , size:%zu , *buf:%p , tid: %d , elapsed time:%d", fd, size, buf, gettid(),cost); return rSize; }
电视桌面监控日志:
notion image
由此便可以收集到应用在文件读写时的相关信息:文件路径、fd、buffer大小、调用栈等,并可以统计耗时、操作次数等。基于这些信息,就可以设定一些策略进行检测判断。
解决的问题:
  • 避免在主线程 进行I/O,特别是SharedPreferences即使是几十 KB 的数据,也能可能造成卡顿。
  • 读写 Buffer 过小: 如果我们的 Buffer 太小,会导致多次无用的系统调用和内存拷贝,导致读写的次数增多,从而影响了性能。
  • 重复读:如果频繁地读取某个文件,并且这个文件一直没有被写入更新,我们可以通过缓存来提升性能。(使用内存缓存避免频繁读取)
  • 资源泄漏: 指打开资源包括文件、Cursor 等没有及时 close,从而引起泄露。这属于非常低级的编码错误,但却非常普遍存在。

5、优化与建议

  • 选用合适的 IO 方式,对于读写速度要求高, 使用缓存 IO;
    • SharedPreferences可以替换成MMKV;
    • 对于频繁读写的大文件, 比如存储Log,使用mmap;
    • Buffer缓冲区的获取与磁盘块大小一致比较合适。
  • 使用Okio 中ByteString 和 Buffer 通过重用等技巧, 很大程度上减少处理器和内存的消耗;
  • 优化数据结构与业务逻辑,降低I/O操作的频率。

6、参考

  1. Linux Storage Stack Diagram
  1. 清河大学-Android开发高手课
  1. Linux Performance (brendangregg.com)
  1. https://netflixtechblog.com/linux-performance-analysis-in-60-000-milliseconds-accc10403c55
  1. Android 内核文件系统支持| Android Open Source Project
  1. Android Native Hook简介-技术圈
  • 作者:白色风车
  • 链接:https://f.appa.me/article/xhook_io
  • 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章
Android U新特性 - Modern广播队列机制介绍
案例:数据分析
AIGC持续火爆,生成式AI的应用场景有哪些?
加锁文章2
持续调教prompt(案例)
还有不知道 commit 规范 ?
Android HiltGPT简介
Loading...
目录
0%
1、概述2、I/O基础2.1 I/O的分类2.2 文件系统2.3 磁盘3、性能分析4、文件的监控5、优化与建议6、参考
白色风车
白色风车
为了不折腾而折腾
文章
17
分类
4
标签
8
最新发布
CD/CD
CD/CD
2024-8-21
AIGC持续火爆,生成式AI的应用场景有哪些?
AIGC持续火爆,生成式AI的应用场景有哪些?
2024-8-21
GPT辅助润色论文
GPT辅助润色论文
2024-8-21
基本提示模式
基本提示模式
2024-8-21
陆奇-新范式 新时代 新机会
陆奇-新范式 新时代 新机会
2024-8-21
Android Hilt
Android Hilt
2024-8-21
公告
🏄遥遥领先🏄
为了不折腾而折腾
 
目录
0%
1、概述2、I/O基础2.1 I/O的分类2.2 文件系统2.3 磁盘3、性能分析4、文件的监控5、优化与建议6、参考
2013-2025 白色风车.