为什么监控文件读写
在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
int open(const char *pathname, int flags, mode_t mode);
ssize_t read(int fd, void *buf, size_t size);
ssize_t write(int fd, const void *buf, size_t size); write_cuk
int close(int fd);
这些函数的实现, 分别在 libopenjdkjvm.so, libjavacore.so 和 libopenjdk.so 中, 覆盖所有的 java 层 IO 调用,具体可以参考io_canary_jni.cc,所以hook住这些代码,就可以对所有的IO操作了如指掌。
直接看Matrix如何hook的
先看下整个项目的结构,分为两部分,一部分java实现,一部分c++实现
java部分
@Override
public void init(Application app, PluginListener listener) {
super.init(app, listener);
IOCanaryUtil.setPackageName(app);
mCore = new IOCanaryCore(this);
}
通过初始化,我们看到IOCanaryCore类,这个就是核心的类,直接看它的start函数
public void start() {
initDetectorsAndHookers(mIOConfig);
synchronized (this) {
mIsStart = true;
}
}
// 开始hook逻辑
private void initDetectorsAndHookers(IOConfig ioConfig) {
assert ioConfig != null;
// 如果配置中,允许检测文件主线程IO或者允许检测缓冲区或者允许重复读取相同文件
if (ioConfig.isDetectFileIOInMainThread()
|| ioConfig.isDetectFileIOBufferTooSmall()
|| ioConfig.isDetectFileIORepeatReadSameFile()) {
IOCanaryJniBridge.install(ioConfig, this);
}
// 如果只检测可关闭的泄漏,使用closeguarhooker比较好
//if only detect io closeable leak use CloseGuardHooker is Better
if (ioConfig.isDetectIOClosableLeak()) {
mCloseGuardHooker = new CloseGuardHooker(this);
mCloseGuardHooker.hook();
}
}
// IOCanaryJniBridge.install
public static void install(IOConfig config, OnJniIssuePublishListener listener) {
MatrixLog.v(TAG, "install sIsTryInstall:%b", sIsTryInstall);
if (sIsTryInstall) {
return;
}
// 加载 io-canary so库
//load lib
if (!loadJni()) {
MatrixLog.e(TAG, "install loadJni failed");
return;
}
//set listener
sOnIssuePublishListener = listener;
try {
//set config
if (config != null) {
if (config.isDetectFileIOInMainThread()) {
// static native void enableDetector() native方法
enableDetector(DetectorType.MAIN_THREAD_IO);
// ms to μs
setConfig(ConfigKey.MAIN_THREAD_THRESHOLD, config.getFileMainThreadTriggerThreshold() * 1000L);
}
if (config.isDetectFileIOBufferTooSmall()) {
enableDetector(DetectorType.SMALL_BUFFER);
setConfig(ConfigKey.SMALL_BUFFER_THRESHOLD, config.getFileBufferSmallThreshold());
}
if (config.isDetectFileIORepeatReadSameFile()) {
enableDetector(DetectorType.REPEAT_READ);
setConfig(ConfigKey.REPEAT_READ_THRESHOLD, config.getFileRepeatReadThreshold());
}
}
//hook
//重点hook来了
doHook();
sIsTryInstall = true;
} catch (Error e) {
MatrixLog.printErrStackTrace(TAG, e, "call jni method error");
}
}
hoot通过JNI调用
static native boolean doHook()
直接转C层实现
JNIEXPORT jboolean JNICALL
Java_com_tencent_matrix_iocanary_core_IOCanaryJniBridge_doHook(JNIEnv *env, jclass type) {
// Android日志
__android_log_print(ANDROID_LOG_INFO, kTag, "doHook");
for (int i = 0; i < TARGET_MODULE_COUNT; ++i) {
// "libopenjdkjvm.so","libjavacore.so","libopenjdk.so"
// 拿到上面其中一个
const char* so_name = TARGET_MODULES[i];
// 打印日志
__android_log_print(ANDROID_LOG_INFO, kTag, "try to hook function in %s.", so_name);
// 打开so库,获取相关信息
void* soinfo = xhook_elf_open(so_name);
if (!soinfo) {
__android_log_print(ANDROID_LOG_WARN, kTag, "Failure to open %s, try next.", so_name);
continue;
}
// hook open函数 替换为ProxyOpen函数
xhook_hook_symbol(soinfo, "open", (void*)ProxyOpen, (void**)&original_open);
xhook_hook_symbol(soinfo, "open64", (void*)ProxyOpen64, (void**)&original_open64);
bool is_libjavacore = (strstr(so_name, "libjavacore.so") != nullptr);
if (is_libjavacore) {
// hook read函数
if (xhook_hook_symbol(soinfo, "read", (void*)ProxyRead, (void**)&original_read) != 0) {
__android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook read failed, try __read_chk");
if (xhook_hook_symbol(soinfo, "__read_chk", (void*)ProxyReadChk, (void**)&original_read_chk) != 0) {
__android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __read_chk");
xhook_elf_close(soinfo);
return JNI_FALSE;
}
}
// hook write函数
if (xhook_hook_symbol(soinfo, "write", (void*)ProxyWrite, (void**)&original_write) != 0) {
__android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook write failed, try __write_chk");
if (xhook_hook_symbol(soinfo, "__write_chk", (void*)ProxyWriteChk, (void**)&original_write_chk) != 0) {
__android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __write_chk");
xhook_elf_close(soinfo);
return JNI_FALSE;
}
}
}
// hook close函数
xhook_hook_symbol(soinfo, "close", (void*)ProxyClose, (void**)&original_close);
xhook_elf_close(soinfo);
}
__android_log_print(ANDROID_LOG_INFO, kTag, "doHook done.");
return JNI_TRUE;
}
来看一个hook函数
ssize_t ProxyRead(int fd, void *buf, size_t size) {
// 如果不是主线程, 则直接调用原始方法
if(!IsMainThread()) {
return original_read(fd, buf, size);
}
// 开始时间
int64_t start = GetTickCountMicros();
// 调用原始方法
size_t ret = original_read(fd, buf, size);
// 结束时间
long read_cost_us = GetTickCountMicros() - start;
//__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);
iocanary::IOCanary::Get().OnRead(fd, buf, size, ret, read_cost_us);
return ret;
}
是不是跟java字节码插桩的时候一样,在原始的函数调用前后插入代码来计算时间。这里面有个优化点是只做了主线程的计算,如果子线程中没有耗时操作,那么就跟什么都没发生一样。你是还想知道数据在哪里处理的呢?
// 其实每当文件close之后会调用OnClose,然后调用下面方法OfferFileIOInfo
// 将文件io的信息加入到一个队列中queue_
void IOCanary::OfferFileIOInfo(std::shared_ptr<IOInfo> file_io_info) {
std::unique_lock<std::mutex> lock(queue_mutex_);
queue_.push_back(file_io_info);
queue_cv_.notify_one();
lock.unlock();
}
在Detect方法中,不断的从队列中取出file_io_info。
void IOCanary::Detect() {
std::vector<Issue> published_issues;
std::shared_ptr<IOInfo> file_io_info;
// 循环启动
while (true) {
published_issues.clear();
int ret = TakeFileIOInfo(file_io_info);
if (ret != 0) {
break;
}
for (auto detector : detectors_) {
detector->Detect(env_, *file_io_info, published_issues);
}
if (issued_callback_ && !published_issues.empty()) {
// 如果检测出的问题不为空,回调给java层
issued_callback_(published_issues);
}
file_io_info = nullptr;
}
}
// Detect 在IOCanary对象创建的时候就开启,不断的从队列中取出数据,检测,然后上报。
IOCanary::IOCanary() {
exit_ = false;
std::thread detect_thread(&IOCanary::Detect, this);
detect_thread.detach();
}
总结
简单做个总结:
- 只在主线程中检测IO操作,保证主线程中IO操作处于正常范围。
- 接触了新的c++代码hook技术,xhook 官方地址:https://github.com/iqiyi/xHook