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

4,864 阅读6分钟

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

  • 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 值一样)

image.png

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

image.png

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

image.png

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

image.png

方案

方案:直接开个线程,每 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 线程:

val threadGroup: ThreadGroup = Looper.getMainLooper().thread.threadGroup
val threadList = arrayOfNulls<Thread>(threadGroup.activeCount() * 2)
val size = threadGroup.enumerate(threadList);

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

File(String.format("/proc/%s/status", Process.myPid())).forEachLine { line ->
    when {
        line.startsWith("Threads") -> {
            Log.d("mjzuo", line)
        }
    }
}

方案:关于监控线程数量的监控,与 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 接口:

JNIEXPORT void JNICALL
Java_com_kwai_performance_overhead_thread_monitor_NativeHandler_start(
    JNIEnv *env, jclass obj) {
  koom::Log::info("koom-thread", "start");
  koom::Start();
}

JNIEXPORT void JNICALL
Java_com_kwai_performance_overhead_thread_monitor_NativeHandler_stop(
    JNIEnv *env, jclass obj) {
  koom::Stop();
}

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

void Start() {
  if (isRunning) {
    return;
  }
  // 初始化数据
  delete sHookLooper;
  sHookLooper = new HookLooper(); // 创建 HookLooper 用来转发消息
  koom::ThreadHooker::Start(); // 开始 hook 
  isRunning = true;
}

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

void ThreadHooker::Start() { ThreadHooker::InitHook(); }

void ThreadHooker::InitHook() {
  koom::Log::info(thread_tag, "HookSo init hook");
  std::set<std::string> libs;
  DlopenCb::GetInstance().GetLoadedLibs(libs); // 拿到要被hook的动态库
  HookLibs(libs, Constant::kDlopenSourceInit); // hook
  DlopenCb::GetInstance().AddCallback(DlopenCallback); // 监听,其中GetLoadedLibs(libs, true) 才会回调
}

这是 thread_hook.cpp#HookLibs 接口

void ThreadHooker::HookLibs(std::set<std::string> &libs, int source) {
  koom::Log::info(thread_tag, "HookSo lib size %d", libs.size());
  if (libs.empty()) {
    return;
  }
  bool hooked = false;
  pthread_mutex_lock(&DlopenCb::hook_mutex);
  xhook_clear(); // 清除 xhook 的缓存,重置所有的全局标示
  for (const auto &lib : libs) {
    hooked |= ThreadHooker::RegisterSo(lib, source); // 开始 hook so 方法
  }
  if (hooked) {
    int result = xhook_refresh(0); // 0:表示执行同步的 hook 操作,1:表示执行异步的 hook 操作
    koom::Log::info(thread_tag, "HookSo lib Refresh result %d", result);
  }
  pthread_mutex_unlock(&DlopenCb::hook_mutex);
}

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

bool ThreadHooker::RegisterSo(const std::string &lib, int source) {
  if (IsLibIgnored(lib)) { // 过滤不hook的库,不贴了
    return false;
  }
  auto lib_ctr = lib.c_str();
  koom::Log::info(thread_tag, "HookSo %d %s", source, lib_ctr);
  xhook_register(lib_ctr, "pthread_create",
                 reinterpret_cast<void *>(HookThreadCreate), nullptr);
  xhook_register(lib_ctr, "pthread_detach",
                 reinterpret_cast<void *>(HookThreadDetach), nullptr);
  xhook_register(lib_ctr, "pthread_join",
                 reinterpret_cast<void *>(HookThreadJoin), nullptr);
  xhook_register(lib_ctr, "pthread_exit",
                 reinterpret_cast<void *>(HookThreadExit), nullptr);

  return true;
}

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

int ThreadHooker::HookThreadCreate(pthread_t *tidp, const pthread_attr_t *attr,
                                   void *(*start_rtn)(void *), void *arg) {
  if (hookEnabled() && start_rtn != nullptr) {
    ... // hook 返回的信息
    if (thread != nullptr) { 
      koom::CallStack::JavaStackTrace(thread, hook_arg->thread_create_arg->java_stack); // java栈
    }
    koom::CallStack::FastUnwind(thread_create_arg->pc, koom::Constant::kMaxCallStackDepth); // native 栈回溯
    thread_create_arg->stack_time = Util::CurrentTimeNs() - time; 
    return pthread_create(tidp, attr,
                          reinterpret_cast<void *(*)(void *)>(HookThreadStart),
                          reinterpret_cast<void *>(hook_arg));
  }
  return pthread_create(tidp, attr, start_rtn, arg);
}

随后调用 thread_hook.cpp#HookThreadStart

ALWAYS_INLINE void ThreadHooker::HookThreadStart(void *arg) {
  ... // 拿hook信息,组HookAddInfo,具体不贴了
  auto info = new HookAddInfo(tid, Util::CurrentTimeNs(), self,
                              state == PTHREAD_CREATE_DETACHED,
                              hookArg->thread_create_arg);

  sHookLooper->post(ACTION_ADD_THREAD, info); // 转发 HookLooper.cpp#handle
  void *(*start_rtn)(void *) = hookArg->start_rtn;
  void *routine_arg = hookArg->arg;
  delete hookArg;
  start_rtn(routine_arg);
}

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

case ACTION_ADD_THREAD: {
  koom::Log::info(looper_tag, "AddThread");
  auto info = static_cast<HookAddInfo *>(data);
  holder->AddThread(info->tid, info->pthread, info->is_thread_detached,
                    info->time, info->create_arg); // 再转发
  delete info;
  break;
}

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

void ThreadHolder::AddThread(int tid, pthread_t threadId, bool isThreadDetached,
                             int64_t start_time, ThreadCreateArg *create_arg) {
  bool valid = threadMap.count(threadId) > 0;
  if (valid) return;

  koom::Log::info(holder_tag, "AddThread tid:%d pthread_t:%p", tid, threadId);
  auto &item = threadMap[threadId]; // 线程列表
  item.Clear();
  item.thread_internal_id = threadId;
  item.thread_detached = isThreadDetached; // 这个就是上面提到的线程状态,false
  item.startTime = start_time;
  item.create_time = create_arg->time;
  item.id = tid;
  ... // 栈内容就不贴了
  delete create_arg;
  koom::Log::info(holder_tag, "AddThread finish");
}

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

void ThreadHolder::DetachThread(pthread_t threadId) {
  bool valid = threadMap.count(threadId) > 0;
  koom::Log::info(holder_tag, "DetachThread tid:%p", threadId);
  if (valid) {
    threadMap[threadId].thread_detached = true; // 将状态改变
  } else {
    leakThreadMap.erase(threadId); // 从泄漏线程列表中移除
  }
}

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

void ThreadHolder::ExitThread(pthread_t threadId, std::string &threadName,
                              long long int time) {
  bool valid = threadMap.count(threadId) > 0;
  if (!valid) return;
  auto &item = threadMap[threadId];
  ...
  if (!item.thread_detached) {
    // 泄露了
    koom::Log::error(holder_tag,
                     "Exited thread Leak! Not joined or detached!\n tid:%p",
                     threadId);
    leakThreadMap[threadId] = item;
  }
  threadMap.erase(threadId); // 从线程集合移除
  koom::Log::info(holder_tag, "ExitThread finish");
}

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

本节完。

参考:

cloud.tencent.com/developer/a…