背景
在某项目实际开发一个显示相关功能过程中,出现了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函数里面完成的,有了这些基础下面就开始正式分析了。
内核调用栈分析
- 首先在分配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);
- 抓取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 ]---
用户调用栈分析
- 通过内核栈分析,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
- 从调用栈可以确定HWDeviceDRM::Commit里面直接调用了dup系统调用导致了泄漏,这和之前的分析吻合,问题点范围缩小了很多,就是在HWDeviceDRM::Commit里面。
代码分析
- 至此问题基本已经锁定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;
}
- 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);
}
}
}
。。。
}
- 再次运行抓取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,泄漏就没有了,至此这个问题完美解决了。