当前位置:   article > 正文

修炼系列(33),内存监控技术方案(上)_xhook监控fd

xhook监控fd

本系列将围绕下面几个方面来介绍内存监控方案:

  • FD 数量

  • 线程数量

  • 虚拟内存

  • java 堆

  • Native 内存

FD 监控

FD 即 File Descriptor (文件描述符),对于 Android 来说,一个进程能使用的 FD 资源是有限的,在 Android9 前,最多限制 1024,Android9 及以上,最多 3w 余个。而 FD 达到上限后,没资源了就会产生各种问题,跟 OOM 一样,很难被定位到,因为 crash 后的堆栈信息可能并没指向“始作俑者”。所以 FD 泄漏的监控是很有必要的。

那什么操作会占用 FD 资源呢?常见的:文件读写、Socket 通信、创建 java 线程、启用 HandlerThread、创建 Window 、数据库操作等。

以创建 java 线程为例,创建线程首先会在 Native 层创建 JNIEnv,这步包括:

  1. 通过匿名共享内存分配 4KB 的内核态内存。
  2. 通过 mmap 映射到用户态虚拟内存地址空间。

其中在创建匿名共享内存时,会打开 /dev/ashmem 文件,所以创建线程需要一个 FD。

FD 信息

我们通过读取 /proc 下的虚拟文件来获取进程的 FD, 代码可见matrix,具体方法见下:

  • 读取进程状态 /proc/pid/limits, 并解释 limit.rlim_max 字段。(我实际测了下,rlim_cur 和 rlim_max 值一样)

读取进程文件 /proc/pid/fd, 计算文件数量。

遍历进程文件 /proc/pid/fd,并通过 readlink 解释文件链接。(在 RequiresApi 21 及以上可以直接使用系统方法 Os.readlink(file.absolutePath))

不清楚怎么调用 c++ 代码的,可以看下我之前博客,Android修炼系列(十九),来编译一个自己的 so 库吧,在我的三星S8测试机上部分数据如下: 

 

方案

方案:直接开个线程,每 10s 周期检查一次当前进程 FD 数量,当 FD 数量达到阈值时(如90%),就抓取一次当前进程的 FD 信息、线程信息、内存快照信息。

我们可以拿 FD 信息内的路径,用来定位 IO 问题,通过线程名称,来定位 java 线程和 HandlerThread 的问题,通过内存快照来排查 Socket 和 window 等问题。

关于如何 dumpHprofData 内存快照,后面会单独写一节。

线程监控

每个线程都对应着一个栈内存,在 Android 中,一个 java 线程大概占用 1M 栈内存,如果是 native 线程,可以通过 pthread_atta_t 来指定栈大小,如果不加限制的创建线程,就会导致 OOM crash

系统从下面 3 个方面限制了线程的数量:

  • 配置文件 /proc/sys/kernel/threads-max 指定了系统范围内的最大线程数量。

  • Linux resource limits 的 RLIMIT_NPROC 参数对应了应用的最大线程数量。

  • 虚拟地址空间不足或内核分配 vma 失败等内存原因,也限制了能创建的线程数量。

试了下直接读取 threads-max 文件,没有权限诶。

线程信息

我们可以通过 ThreadGroup 来获取所有 java 线程:

  1. val threadGroup: ThreadGroup = Looper.getMainLooper().thread.threadGroup
  2. val threadList = arrayOfNulls<Thread>(threadGroup.activeCount() * 2)
  3. val size = threadGroup.enumerate(threadList);
  4. 复制代码

native 的线程数量,我们可以读取 /proc/[ pid ]/status 中的 Threads 字段的值,其中 /proc/[ pid ]/task 目录下记录着所有线程的 tid、线程名等信息:

  1. File(String.format("/proc/%s/status", Process.myPid())).forEachLine { line ->
  2. when {
  3. line.startsWith("Threads") -> {
  4. Log.d("mjzuo", line)
  5. }
  6. }
  7. }
  8. 复制代码

方案:关于监控线程数量的监控,与 FD 的思想一样,都是开一个子线程,周期检查应用的当前线程数,当超过阈值时,抓取线程信息并上报。

线程泄漏

不管java 线程,还是 native 线程都是通过 pthread_create 方法创建的。常见的还有 pthread_detach、pthread_join、pthread_exit API,当我们通过 pthread_create 来创建线程时,线程状态默认都是 joinable 状态的,只有 detach 状态的线程,才能在线程执行完退出时自动释放栈内存,否则就需要等待调用 join 来释放内存。

即 create 线程后,不调用 detach 或 join 就直接 exit 退出,栈内存不会释放,会造成线程泄漏。

既然知道技术原理了,那么监控手段就呼之欲出了,hook 上面几个接口,记录 joinable 状态的泄漏线程信息。 以 KOOM源码为例:

java 层的代码就不说了,直接看下 c++ 的逻辑吧,这是桥梁 JNI 接口:

  1. JNIEXPORT void JNICALL
  2. Java_com_kwai_performance_overhead_thread_monitor_NativeHandler_start(
  3. JNIEnv *env, jclass obj) {
  4. koom::Log::info("koom-thread", "start");
  5. koom::Start();
  6. }
  7. JNIEXPORT void JNICALL
  8. Java_com_kwai_performance_overhead_thread_monitor_NativeHandler_stop(
  9. JNIEnv *env, jclass obj) {
  10. koom::Stop();
  11. }
  12. 复制代码

我们来看下 koom.cpp#Start 接口:

  1. void Start() {
  2. if (isRunning) {
  3. return;
  4. }
  5. // 初始化数据
  6. delete sHookLooper;
  7. sHookLooper = new HookLooper(); // 创建 HookLooper 用来转发消息
  8. koom::ThreadHooker::Start(); // 开始 hook
  9. isRunning = true;
  10. }
  11. 复制代码

这是 thread_hook.cpp#Start 接口,其中dlopencb.h 的逻辑就不贴了,目录在 koom-common/third-party/xhook/src/main/cpp/xhook/src/ :

  1. void ThreadHooker::Start() { ThreadHooker::InitHook(); }
  2. void ThreadHooker::InitHook() {
  3. koom::Log::info(thread_tag, "HookSo init hook");
  4. std::set<std::string> libs;
  5. DlopenCb::GetInstance().GetLoadedLibs(libs); // 拿到要被hook的动态库
  6. HookLibs(libs, Constant::kDlopenSourceInit); // hook
  7. DlopenCb::GetInstance().AddCallback(DlopenCallback); // 监听,其中GetLoadedLibs(libs, true) 才会回调
  8. }
  9. 复制代码

这是 thread_hook.cpp#HookLibs 接口

  1. void ThreadHooker::HookLibs(std::set<std::string> &libs, int source) {
  2. koom::Log::info(thread_tag, "HookSo lib size %d", libs.size());
  3. if (libs.empty()) {
  4. return;
  5. }
  6. bool hooked = false;
  7. pthread_mutex_lock(&DlopenCb::hook_mutex);
  8. xhook_clear(); // 清除 xhook 的缓存,重置所有的全局标示
  9. for (const auto &lib : libs) {
  10. hooked |= ThreadHooker::RegisterSo(lib, source); // 开始 hook so 方法
  11. }
  12. if (hooked) {
  13. int result = xhook_refresh(0); // 0:表示执行同步的 hook 操作,1:表示执行异步的 hook 操作
  14. koom::Log::info(thread_tag, "HookSo lib Refresh result %d", result);
  15. }
  16. pthread_mutex_unlock(&DlopenCb::hook_mutex);
  17. }
  18. 复制代码

这是我们要hook 的方法:thread_hook.cpp#RegisterSo

  1. bool ThreadHooker::RegisterSo(const std::string &lib, int source) {
  2. if (IsLibIgnored(lib)) { // 过滤不hook的库,不贴了
  3. return false;
  4. }
  5. auto lib_ctr = lib.c_str();
  6. koom::Log::info(thread_tag, "HookSo %d %s", source, lib_ctr);
  7. xhook_register(lib_ctr, "pthread_create",
  8. reinterpret_cast<void *>(HookThreadCreate), nullptr);
  9. xhook_register(lib_ctr, "pthread_detach",
  10. reinterpret_cast<void *>(HookThreadDetach), nullptr);
  11. xhook_register(lib_ctr, "pthread_join",
  12. reinterpret_cast<void *>(HookThreadJoin), nullptr);
  13. xhook_register(lib_ctr, "pthread_exit",
  14. reinterpret_cast<void *>(HookThreadExit), nullptr);
  15. return true;
  16. }
  17. 复制代码

当调用 pthread_create 方法时,会被拦截进我们hook的方法:

  1. int ThreadHooker::HookThreadCreate(pthread_t *tidp, const pthread_attr_t *attr,
  2. void *(*start_rtn)(void *), void *arg) {
  3. if (hookEnabled() && start_rtn != nullptr) {
  4. ... // hook 返回的信息
  5. if (thread != nullptr) {
  6. koom::CallStack::JavaStackTrace(thread, hook_arg->thread_create_arg->java_stack); // java栈
  7. }
  8. koom::CallStack::FastUnwind(thread_create_arg->pc, koom::Constant::kMaxCallStackDepth); // native 栈回溯
  9. thread_create_arg->stack_time = Util::CurrentTimeNs() - time;
  10. return pthread_create(tidp, attr,
  11. reinterpret_cast<void *(*)(void *)>(HookThreadStart),
  12. reinterpret_cast<void *>(hook_arg));
  13. }
  14. return pthread_create(tidp, attr, start_rtn, arg);
  15. }
  16. 复制代码

随后调用 thread_hook.cpp#HookThreadStart

  1. ALWAYS_INLINE void ThreadHooker::HookThreadStart(void *arg) {
  2. ... // 拿hook信息,组HookAddInfo,具体不贴了
  3. auto info = new HookAddInfo(tid, Util::CurrentTimeNs(), self,
  4. state == PTHREAD_CREATE_DETACHED,
  5. hookArg->thread_create_arg);
  6. sHookLooper->post(ACTION_ADD_THREAD, info); // 转发 HookLooper.cpp#handle
  7. void *(*start_rtn)(void *) = hookArg->start_rtn;
  8. void *routine_arg = hookArg->arg;
  9. delete hookArg;
  10. start_rtn(routine_arg);
  11. }
  12. 复制代码

消息被转发到 HookLooper.cpp#handle:

  1. case ACTION_ADD_THREAD: {
  2. koom::Log::info(looper_tag, "AddThread");
  3. auto info = static_cast<HookAddInfo *>(data);
  4. holder->AddThread(info->tid, info->pthread, info->is_thread_detached,
  5. info->time, info->create_arg); // 再转发
  6. delete info;
  7. break;
  8. }
  9. 复制代码

消息被转发到 thread_holder.cpp#AddThread,在这里就记录了线程,并标记状态:

  1. void ThreadHolder::AddThread(int tid, pthread_t threadId, bool isThreadDetached,
  2. int64_t start_time, ThreadCreateArg *create_arg) {
  3. bool valid = threadMap.count(threadId) > 0;
  4. if (valid) return;
  5. koom::Log::info(holder_tag, "AddThread tid:%d pthread_t:%p", tid, threadId);
  6. auto &item = threadMap[threadId]; // 线程列表
  7. item.Clear();
  8. item.thread_internal_id = threadId;
  9. item.thread_detached = isThreadDetached; // 这个就是上面提到的线程状态,false
  10. item.startTime = start_time;
  11. item.create_time = create_arg->time;
  12. item.id = tid;
  13. ... // 栈内容就不贴了
  14. delete create_arg;
  15. koom::Log::info(holder_tag, "AddThread finish");
  16. }
  17. 复制代码

其他方法就不细说了,我们来看下当消息被转发过来时,detach 和 join 的逻辑是一样的,所以就贴一个了:

  1. void ThreadHolder::DetachThread(pthread_t threadId) {
  2. bool valid = threadMap.count(threadId) > 0;
  3. koom::Log::info(holder_tag, "DetachThread tid:%p", threadId);
  4. if (valid) {
  5. threadMap[threadId].thread_detached = true; // 将状态改变
  6. } else {
  7. leakThreadMap.erase(threadId); // 从泄漏线程列表中移除
  8. }
  9. }
  10. 复制代码

这是 exit 的逻辑,在这里将非 detached 状态的线程都加入到泄漏集合里,注意如果 exit 后面再调用 join 还是可以移除掉的:

  1. void ThreadHolder::ExitThread(pthread_t threadId, std::string &threadName,
  2. long long int time) {
  3. bool valid = threadMap.count(threadId) > 0;
  4. if (!valid) return;
  5. auto &item = threadMap[threadId];
  6. ...
  7. if (!item.thread_detached) {
  8. // 泄露了
  9. koom::Log::error(holder_tag,
  10. "Exited thread Leak! Not joined or detached!\n tid:%p",
  11. threadId);
  12. leakThreadMap[threadId] = item;
  13. }
  14. threadMap.erase(threadId); // 从线程集合移除
  15. koom::Log::info(holder_tag, "ExitThread finish");
  16. }
  17. 复制代码

受篇幅影响(os内心:累了,不想再爱了),虚拟内存、java堆、native 内存监控的内容会放在下节。

本节完。

参考:

cloud.tencent.com/developer/a…

作者:Zuo
链接:https://juejin.cn/post/7080461351474167844
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/341371
推荐阅读
相关标签
  

闽ICP备14008679号