一个FD泄漏问题的定位过程

背景

在某项目实际开发一个显示相关功能过程中,出现了FD泄漏的问题。随便操作几个界面后,HWC进程出现大量的FD的泄露,很快就超过200个。此项目的硬件平台是高通CPU,软件是Android 10的平台,内核版本是Linux 4.19,在高通平台上,显示模块的HAL层服务是vendor.qti.hardware.display.composer-service,简称HWC。

ruby 复制代码
vendor.qti.hardware.display.composer-service的进程号是864

XXXXXX:/proc/864/fd # ls -l
//存在大量的anon_inode:sync_file文件句柄
lr-x------ 1 system graphics 64 2022-07-29 00:04 342 -> anon_inode:sync_file

分析过程

初步分析

因为泄漏fd对应的文件名是anon_inode:sync_file,并且发生在HWC进程中,因此可以大致确定和Fence相关。然而HWC里面对Fence的操作地方太多,直接从HWC下手定位较为困难。不过由于泄露速度很快且可以稳定复现,因此可以通过一些调试手段来缩小范围。

分析的思路就是在监测到FD泄漏时,把调用者找出来。首先需要找到FD的分配地方,在Linux里面FD是属于进程独立的,即每个进程都有独立的FD,是一个正整数,普通文件从3开始,FD的分配是在get_unused_fd_flags函数里面完成的,有了这些基础下面就开始正式分析了。

内核调用栈分析

  1. 首先在分配FD的位置加入打印调用栈信息,不过由于get_unused_fd_flags的调用点太多,因此设定500作为触发条件,这样就只有泄露FD的调用栈被输出了。在这里定义了一个函数trace_for_leak,在发生泄漏时打印一下调用栈。
scss 复制代码
int trace_for_leak(int fd)
{
  printk("trace_for_leak fd:%d", fd);
  WARN(1, "fd:%d", fd);
  return 0;
}
EXPORT_SYMBOL(trace_for_leak);

int get_unused_fd_flags(unsigned flags)
{
  int fd = __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags);
  if(fd > 500) {
    trace_for_leak(fd);
  }
  return fd;
}
EXPORT_SYMBOL(get_unused_fd_flags);
  1. 抓取Log,发现所有的调用栈都是相同的,说明泄漏点只有1个,从调用栈看是用户态调用dup导致,这样就确定了泄漏的系统调用,不过dup在HWC里面的调用也是非常多,还需要继续缩小范围。
ini 复制代码
<KERNEL|(2)HwBinder:878_3 >: [  363.776621] fd:2668
<KERNEL|(2)HwBinder:878_3 >: [  363.776644] WARNING: CPU: 2 PID: 1403 at /home/lizhigang/disk1/src/neo3/kernel/msm-4.19/fs/file.c:545 trace_for_leak+0x34/0x48
<KERNEL|(2)HwBinder:878_3 >: [  363.776647] Modules linked in:
<KERNEL|(2)HwBinder:878_3 >: [  363.776655] CPU: 2 PID: 1403 Comm: HwBinder:878_3 Tainted: G S      W         4.19.81-perf+ #13
<KERNEL|(2)HwBinder:878_3 >: [  363.776658] Hardware name: Qualcomm Technologies, Inc. kona MTP (DT)
<KERNEL|(2)HwBinder:878_3 >: [  363.776663] pstate: 60400005 (nZCv daif +PAN -UAO)
<KERNEL|(2)HwBinder:878_3 >: [  363.776666] pc  trace_for_leak+0x34/0x48
<KERNEL|(2)HwBinder:878_3 >: [  363.776670] lr  trace_for_leak+0x34/0x48
<KERNEL|(2)HwBinder:878_3 >: [  363.776673] sp  ffffff8019519de0
<KERNEL|(2)HwBinder:878_3 >: [  363.776675] x29: ffffff8019519df0 x28: ffffffca9ceb1d80
<KERNEL|(2)HwBinder:878_3 >: [  363.776681] x27: 0000000000000000 x26: 0000000000000000
<KERNEL|(2)HwBinder:878_3 >: [  363.776688] x25: 0000000056000000 x24: 0000000000000000
<KERNEL|(2)HwBinder:878_3 >: [  363.776694] x23: ffffffca9ceb1d80 x22: 0000000000000126
<KERNEL|(2)HwBinder:878_3 >: [  363.776700] x21: 0000000000000017 x20: ffffffc9a8bff900
<KERNEL|(2)HwBinder:878_3 >: [  363.776706] x19: 0000000000000a6c x18: 00000000000000b0
<KERNEL|(2)HwBinder:878_3 >: [  363.776712] x17: 00000000000000b0 x16: 0000000000000028
<KERNEL|(2)HwBinder:878_3 >: [  363.776718] x15: ffffff9816703c9c x14: 0000000000003638
<KERNEL|(2)HwBinder:878_3 >: [  363.776724] x13: 0000000000000004 x12: 0000000000000000
<KERNEL|(2)HwBinder:878_3 >: [  363.776730] x11: 0000000000000001 x10: 0000000000000007
<KERNEL|(2)HwBinder:878_3 >: [  363.776736] x9 : d74c738a6b760b00 x8 : d74c738a6b760b00
<KERNEL|(2)HwBinder:878_3 >: [  363.776742] x7 : 0000000000000000 x6 : ffffff9817a55a9c
<KERNEL|(2)HwBinder:878_3 >: [  363.776748] x5 : ffffff8019519aa8 x4 : 000000000000000c
<KERNEL|(2)HwBinder:878_3 >: [  363.776754] x3 : 0000000038363632 x2 : 000000000000000c
<KERNEL|(2)HwBinder:878_3 >: [  363.776760] x1 : 0000000000000000 x0 : 000000000000000c
<KERNEL|(2)HwBinder:878_3 >: [  363.776767] Call trace:
<KERNEL|(2)HwBinder:878_3 >: [  363.776772] trace_for_leak+0x34/0x48
<KERNEL|(2)HwBinder:878_3 >: [  363.776777] get_unused_fd_flags+0x44/0x54
<KERNEL|(2)HwBinder:878_3 >: [  363.776781] ksys_dup+0x30/0x98
<KERNEL|(2)HwBinder:878_3 >: [  363.776785] __arm64_sys_dup+0x1c/0x2c
<KERNEL|(2)HwBinder:878_3 >: [  363.776792] el0_svc_common+0xa4/0x16c
<KERNEL|(2)HwBinder:878_3 >: [  363.776796] el0_svc_handler+0x7c/0x98
<KERNEL|(2)HwBinder:878_3 >: [  363.776801] el0_svc+0x8/0xc
<KERNEL|(2)HwBinder:878_3 >: [  363.776804] ---[ end trace 8608268fa69eeafc ]---

用户调用栈分析

  1. 通过内核栈分析,FD的泄漏是用户态系统调用直接导致,因此需要抓取用户调用栈,把内核栈和用户栈贯通一起看,完成此功能需要使用ebpf的bpftrace工具。bpftrace可以把内核栈和用户栈一起打印出来,注意需要把用户态的程序替换为包含符号表的程序,否则只能打印出地址,看不出函数名。在Android的开发平台下,包含符号表的可执行程序在./out/target/product/XXXX/symbols/下面,因此用这个目录下面的vendor.qti.hardware.display.composer-service来替换设备上的对应文件。然后执行bpftrace,只有简单的一行指令即可。

bpftrace -e 'kprobe:trace_for_leak { @[probe, pid, tid, kstack,ustack] = count(); }'

下面就是执行的结果

ruby 复制代码
XXXXXXX:/data/local/tmp/bpftools # ./bpftrace -e 'kprobe:trace_for_leak { @[probe, pid, tid, kstack,ustack] = count(); }'
tar: invalid tar format
Attaching 1 probe...
^C

@[kprobe:trace_for_leak, 861, 1022,
    trace_for_leak+0
    ksys_dup+48
    __arm64_sys_dup+28
    el0_svc_common+164
    el0_svc_handler+124
    el0_svc+8
,
    dup+8
    sdm::HWDeviceDRM::Commit(sdm::HWLayers*)+224
    sdm::HWPeripheralDRM::Commit(sdm::HWLayers*)+244
    sdm::DisplayBase::Commit(sdm::LayerStack*)+632
    sdm::DisplayBuiltIn::Commit(sdm::LayerStack*)+1040
    sdm::HWCDisplay::CommitLayerStack()+4228
    sdm::HWCDisplayBuiltIn::Present(int*)+632
    vendor::qti::hardware::display::composer::V2_1::implementation::QtiComposerClient::CommandReader::presentDisplay(unsigned long, int&, std::__1::vector<unsigned long, std::__1::allocator<unsigned long> >&, std::__1::vector<int, std::__1::allocator<int> >&)+2544
    vendor::qti::hardware::display::composer::V2_1::implementation::QtiComposerClient::CommandReader::parseCommonCmd(android::hardware::graphics::composer::V2_3::IComposerClient::Command, unsigned short)+10284
    vendor::qti::hardware::display::composer::V2_1::implementation::QtiComposerClient::CommandReader::parse()+472
    vendor::qti::hardware::display::composer::V2_1::implementation::QtiComposerClient::executeCommands_2_3(unsigned int, android::hardware::hidl_vec<android::hardware::hidl_handle> const&, std::__1::function<void (android::hardware::graphics::composer::V2_1::Error, bool, unsigned int, android::hardware::hidl_vec<android::hardware::hidl_handle> const&)>)+124
    android::hardware::graphics::composer::V2_2::BnHwComposerClient::_hidl_executeCommands_2_2(android::hidl::base::V1_0::BnHwBase*, android::hardware::Parcel const&, android::hardware::Parcel*, std::__1::function<void (android::hardware::Parcel&)>)+532
    vendor::qti::hardware::display::composer::V2_1::BnHwQtiComposerClient::onTransact(unsigned int, android::hardware::Parcel const&, android::hardware::Parcel*, unsigned int, std::__1::function<void (android::hardware::Parcel&)>)+2984
    android::hardware::BHwBinder::transact(unsigned int, android::hardware::Parcel const&, android::hardware::Parcel*, unsigned int, std::__1::function<void (android::hardware::Parcel&)>)+76
    android::hardware::IPCThreadState::getAndExecuteCommand()+1044
    android::hardware::IPCThreadState::joinThreadPool(bool)+156
    0x7dcef2fa58
    android::Thread::_threadLoop(void*)+332
    __pthread_start(void*)+40
    __start_thread+68
]: 216
  1. 从调用栈可以确定HWDeviceDRM::Commit里面直接调用了dup系统调用导致了泄漏,这和之前的分析吻合,问题点范围缩小了很多,就是在HWDeviceDRM::Commit里面。

代码分析

  1. 至此问题基本已经锁定HWDeviceDRM::Commit函数里面,这个函数很简单,看一下每一个子函数,排除了其他2个函数,问题进一步缩小到HWDeviceDRM::AtomicCommit里面
ini 复制代码
DisplayError HWDeviceDRM::Commit(HWLayers *hw_layers) {
  DTRACE_SCOPED();

  DisplayError err = kErrorNone;
  registry_.Register(hw_layers);

  if (default_mode_) {
    err = DefaultCommit(hw_layers);
  } else {
    err = AtomicCommit(hw_layers);
  }

  return err;
}
  1. HWDeviceDRM::AtomicCommit里面的代码有2处调用到Sys::dup_,在对应的位置都加上打印,把FD的值打印出来。
ini 复制代码
DisplayError HWDeviceDRM::AtomicCommit(HWLayers *hw_layers) {
  。。。
  for (uint32_t i = 0; i < hw_layer_info.hw_layers.size(); i++) {
    Layer &layer = hw_layer_info.hw_layers.at(i);
    HWRotatorSession *hw_rotator_session = &hw_layers->config[i].hw_rotator_session;
    if (hw_rotator_session->mode == kRotatorOffline) {
      hw_rotator_session->output_buffer.release_fence_fd = Sys::dup_(release_fence);
      DLOGW("output_buffer fd:%d", hw_rotator_session->output_buffer.release_fence_fd);
    } else {
      layer.input_buffer.release_fence_fd = -1;
      if (!enable_cac_) {
        // 此处dup生成的layer.input_buffer.release_fence_fd没有关闭,造成fd的泄露,
        // 加入打印后fd的值在快速增长
        layer.input_buffer.release_fence_fd = Sys::dup_(release_fence);
        DLOGW("input_buffer fd:%d", layer.input_buffer.release_fence_fd);
      }
    }
  }
  。。。
}
  1. 再次运行抓取Log发现了input_buffer fd在快速的增加,因此确定了FD泄漏的位置。
arduino 复制代码
864   864 W SDM     : HWDeviceDRM::AtomicCommit: input_buffer fd:386
864   864 W SDM     : HWDeviceDRM::AtomicCommit: input_buffer fd:387
864   864 W SDM     : HWDeviceDRM::AtomicCommit: input_buffer fd:388
864   864 W SDM     : HWDeviceDRM::AtomicCommit: input_buffer fd:389
864   864 W SDM     : HWDeviceDRM::AtomicCommit: input_buffer fd:390
864   864 W SDM     : HWDeviceDRM::AtomicCommit: input_buffer fd:391
864   864 W SDM     : HWDeviceDRM::AtomicCommit: input_buffer fd:392
864   864 W SDM     : HWDeviceDRM::AtomicCommit: input_buffer fd:393

总结

经过上面的分析过程,已经准确的定位到了,引起FD泄漏的代码行,原因就是HWDeviceDRM::AtomicCommit函数里面layer.input_buffer.release_fence_fd是产生fd泄漏的根本原因,input_buffer.release_fence_fd没有被正确Close,产生了泄漏。找到根因后的解决就不是问题了,只要正确的close,泄漏就没有了,至此这个问题完美解决了。

相关推荐
JunLan~4 小时前
Rocky Linux 系统安装/部署 Docker
linux·docker·容器
方竞5 小时前
Linux空口抓包方法
linux·空口抓包
海岛日记6 小时前
centos一键卸载docker脚本
linux·docker·centos
AttackingLin7 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python
学Linux的语莫8 小时前
Ansible使用简介和基础使用
linux·运维·服务器·nginx·云计算·ansible
踏雪Vernon8 小时前
[OpenHarmony5.0][Docker][环境]OpenHarmony5.0 Docker编译环境镜像下载以及使用方式
linux·docker·容器·harmonyos
学Linux的语莫9 小时前
搭建服务器VPN,Linux客户端连接WireGuard,Windows客户端连接WireGuard
linux·运维·服务器
legend_jz9 小时前
【Linux】线程控制
linux·服务器·开发语言·c++·笔记·学习·学习方法
Komorebi.py9 小时前
【Linux】-学习笔记04
linux·笔记·学习