为什么监控文件读写

在Linux中,一切皆文件,那么对于进程的运行,就是不停的文件IO来维持的对吗?因此处理好磁盘的读写, 能够提升应用整体的流畅度,所以如果我们能监控一些耗时的文件读写,针对性的优化,是不是就可以提高用户体验呢,答案肯定是可以

如何能监控文件的读写呢

两种思路:Java hook 、Native hook,我们先来看Java hook如何实现。

Java Hook

我们可以通过代码插桩和动态代理两种方式实现
插桩 :无法覆盖所有的 IO 操作, 大量系统代码也存在 IO 操作
动态代理 :不同的 Android 版本, Java 层的API变更比较大, 有大量的版本适配性工作,且很多系统级的 IO 操作, 是直接调用 native 层的函数, 因此只做 java 层的 hook, 无法覆盖所有场景,所以我们就只剩一条路,对native 进程hook

Native Hook

其实Java中的IO操作都会最终调用Linux的open/read/write/close

  1. int open(const char *pathname, int flags, mode_t mode);
  2. ssize_t read(int fd, void *buf, size_t size);
  3. ssize_t write(int fd, const void *buf, size_t size); write_cuk
  4. int close(int fd);

这些函数的实现, 分别在 libopenjdkjvm.so, libjavacore.so 和 libopenjdk.so 中, 覆盖所有的 java 层 IO 调用,具体可以参考io_canary_jni.cc,所以hook住这些代码,就可以对所有的IO操作了如指掌。

直接看Matrix如何hook的

先看下整个项目的结构,分为两部分,一部分java实现,一部分c++实现
image.png

java部分

  1. @Override
  2. public void init(Application app, PluginListener listener) {
  3. super.init(app, listener);
  4. IOCanaryUtil.setPackageName(app);
  5. mCore = new IOCanaryCore(this);
  6. }

通过初始化,我们看到IOCanaryCore类,这个就是核心的类,直接看它的start函数

  1. public void start() {
  2. initDetectorsAndHookers(mIOConfig);
  3. synchronized (this) {
  4. mIsStart = true;
  5. }
  6. }
  7. // 开始hook逻辑
  8. private void initDetectorsAndHookers(IOConfig ioConfig) {
  9. assert ioConfig != null;
  10. // 如果配置中,允许检测文件主线程IO或者允许检测缓冲区或者允许重复读取相同文件
  11. if (ioConfig.isDetectFileIOInMainThread()
  12. || ioConfig.isDetectFileIOBufferTooSmall()
  13. || ioConfig.isDetectFileIORepeatReadSameFile()) {
  14. IOCanaryJniBridge.install(ioConfig, this);
  15. }
  16. // 如果只检测可关闭的泄漏,使用closeguarhooker比较好
  17. //if only detect io closeable leak use CloseGuardHooker is Better
  18. if (ioConfig.isDetectIOClosableLeak()) {
  19. mCloseGuardHooker = new CloseGuardHooker(this);
  20. mCloseGuardHooker.hook();
  21. }
  22. }
  23. // IOCanaryJniBridge.install
  24. public static void install(IOConfig config, OnJniIssuePublishListener listener) {
  25. MatrixLog.v(TAG, "install sIsTryInstall:%b", sIsTryInstall);
  26. if (sIsTryInstall) {
  27. return;
  28. }
  29. // 加载 io-canary so库
  30. //load lib
  31. if (!loadJni()) {
  32. MatrixLog.e(TAG, "install loadJni failed");
  33. return;
  34. }
  35. //set listener
  36. sOnIssuePublishListener = listener;
  37. try {
  38. //set config
  39. if (config != null) {
  40. if (config.isDetectFileIOInMainThread()) {
  41. // static native void enableDetector() native方法
  42. enableDetector(DetectorType.MAIN_THREAD_IO);
  43. // ms to μs
  44. setConfig(ConfigKey.MAIN_THREAD_THRESHOLD, config.getFileMainThreadTriggerThreshold() * 1000L);
  45. }
  46. if (config.isDetectFileIOBufferTooSmall()) {
  47. enableDetector(DetectorType.SMALL_BUFFER);
  48. setConfig(ConfigKey.SMALL_BUFFER_THRESHOLD, config.getFileBufferSmallThreshold());
  49. }
  50. if (config.isDetectFileIORepeatReadSameFile()) {
  51. enableDetector(DetectorType.REPEAT_READ);
  52. setConfig(ConfigKey.REPEAT_READ_THRESHOLD, config.getFileRepeatReadThreshold());
  53. }
  54. }
  55. //hook
  56. //重点hook来了
  57. doHook();
  58. sIsTryInstall = true;
  59. } catch (Error e) {
  60. MatrixLog.printErrStackTrace(TAG, e, "call jni method error");
  61. }
  62. }

hoot通过JNI调用

  1. static native boolean doHook()

直接转C层实现

  1. JNIEXPORT jboolean JNICALL
  2. Java_com_tencent_matrix_iocanary_core_IOCanaryJniBridge_doHook(JNIEnv *env, jclass type) {
  3. // Android日志
  4. __android_log_print(ANDROID_LOG_INFO, kTag, "doHook");
  5. for (int i = 0; i < TARGET_MODULE_COUNT; ++i) {
  6. // "libopenjdkjvm.so","libjavacore.so","libopenjdk.so"
  7. // 拿到上面其中一个
  8. const char* so_name = TARGET_MODULES[i];
  9. // 打印日志
  10. __android_log_print(ANDROID_LOG_INFO, kTag, "try to hook function in %s.", so_name);
  11. // 打开so库,获取相关信息
  12. void* soinfo = xhook_elf_open(so_name);
  13. if (!soinfo) {
  14. __android_log_print(ANDROID_LOG_WARN, kTag, "Failure to open %s, try next.", so_name);
  15. continue;
  16. }
  17. // hook open函数 替换为ProxyOpen函数
  18. xhook_hook_symbol(soinfo, "open", (void*)ProxyOpen, (void**)&original_open);
  19. xhook_hook_symbol(soinfo, "open64", (void*)ProxyOpen64, (void**)&original_open64);
  20. bool is_libjavacore = (strstr(so_name, "libjavacore.so") != nullptr);
  21. if (is_libjavacore) {
  22. // hook read函数
  23. if (xhook_hook_symbol(soinfo, "read", (void*)ProxyRead, (void**)&original_read) != 0) {
  24. __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook read failed, try __read_chk");
  25. if (xhook_hook_symbol(soinfo, "__read_chk", (void*)ProxyReadChk, (void**)&original_read_chk) != 0) {
  26. __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __read_chk");
  27. xhook_elf_close(soinfo);
  28. return JNI_FALSE;
  29. }
  30. }
  31. // hook write函数
  32. if (xhook_hook_symbol(soinfo, "write", (void*)ProxyWrite, (void**)&original_write) != 0) {
  33. __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook write failed, try __write_chk");
  34. if (xhook_hook_symbol(soinfo, "__write_chk", (void*)ProxyWriteChk, (void**)&original_write_chk) != 0) {
  35. __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __write_chk");
  36. xhook_elf_close(soinfo);
  37. return JNI_FALSE;
  38. }
  39. }
  40. }
  41. // hook close函数
  42. xhook_hook_symbol(soinfo, "close", (void*)ProxyClose, (void**)&original_close);
  43. xhook_elf_close(soinfo);
  44. }
  45. __android_log_print(ANDROID_LOG_INFO, kTag, "doHook done.");
  46. return JNI_TRUE;
  47. }

来看一个hook函数

  1. ssize_t ProxyRead(int fd, void *buf, size_t size) {
  2. // 如果不是主线程, 则直接调用原始方法
  3. if(!IsMainThread()) {
  4. return original_read(fd, buf, size);
  5. }
  6. // 开始时间
  7. int64_t start = GetTickCountMicros();
  8. // 调用原始方法
  9. size_t ret = original_read(fd, buf, size);
  10. // 结束时间
  11. long read_cost_us = GetTickCountMicros() - start;
  12. //__android_log_print(ANDROID_LOG_DEBUG, kTag, "ProxyRead fd:%d buf:%p size:%d ret:%d cost:%d", fd, buf, size, ret, read_cost_us);
  13. iocanary::IOCanary::Get().OnRead(fd, buf, size, ret, read_cost_us);
  14. return ret;
  15. }

是不是跟java字节码插桩的时候一样,在原始的函数调用前后插入代码来计算时间。这里面有个优化点是只做了主线程的计算,如果子线程中没有耗时操作,那么就跟什么都没发生一样。你是还想知道数据在哪里处理的呢?

  1. // 其实每当文件close之后会调用OnClose,然后调用下面方法OfferFileIOInfo
  2. // 将文件io的信息加入到一个队列中queue_
  3. void IOCanary::OfferFileIOInfo(std::shared_ptr<IOInfo> file_io_info) {
  4. std::unique_lock<std::mutex> lock(queue_mutex_);
  5. queue_.push_back(file_io_info);
  6. queue_cv_.notify_one();
  7. lock.unlock();
  8. }

在Detect方法中,不断的从队列中取出file_io_info。

  1. void IOCanary::Detect() {
  2. std::vector<Issue> published_issues;
  3. std::shared_ptr<IOInfo> file_io_info;
  4. // 循环启动
  5. while (true) {
  6. published_issues.clear();
  7. int ret = TakeFileIOInfo(file_io_info);
  8. if (ret != 0) {
  9. break;
  10. }
  11. for (auto detector : detectors_) {
  12. detector->Detect(env_, *file_io_info, published_issues);
  13. }
  14. if (issued_callback_ && !published_issues.empty()) {
  15. // 如果检测出的问题不为空,回调给java层
  16. issued_callback_(published_issues);
  17. }
  18. file_io_info = nullptr;
  19. }
  20. }
  21. // Detect 在IOCanary对象创建的时候就开启,不断的从队列中取出数据,检测,然后上报。
  22. IOCanary::IOCanary() {
  23. exit_ = false;
  24. std::thread detect_thread(&IOCanary::Detect, this);
  25. detect_thread.detach();
  26. }

到目前,大致逻辑已经搞明白了。

总结

简单做个总结:

  • 只在主线程中检测IO操作,保证主线程中IO操作处于正常范围。
  • 接触了新的c++代码hook技术,xhook 官方地址:https://github.com/iqiyi/xHook