免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Android Binder调试全攻略:内核节点、dumpsys、strace与线上故障排查

Android Binder调试全攻略:内核节点、dumpsys、strace与线上故障排查 聊到Binder调试这个话题我相信不少做Android的朋友都有过类似的体验平时写应用层代码Binder就像空气一样透明大家只知道“系统服务是跨进程调用的”但真到了线上出问题的时候——进程频繁重启、主线程卡死、系统服务连不上、接口莫名超时——一抓日志全是binder相关看不懂也不知道从哪查起。去年我们有个线上case主进程每隔几分钟就重启一次tombstone里面全是binder线程block的信息排查了两天最后定位到是三方SDK持有一个系统服务的Binder代理不释放把进程的Binder线程池全部占满了。处理完那个问题之后我花了不少时间把Binder调试相关的知识系统整理了一遍写成系列里的第14篇。这篇文章不聊Binder原理的基础概念直接围绕“调试”这件事把内核节点、dumpsys、strace、日志分析、以及常见故障的排查路径全部过一遍希望能给正在被Binder问题折磨的同学一个能直接抄作业的参考。1. 调试Binder前必须理清的三个认知点1.1 Binder不是“一个东西”而是三层对象的协作我见过太多人排查Binder问题的时候脑子里只有一个模糊的“binder”概念结果看到日志里的handle、node、ref、transaction这些词就懵了。其实Binder体系解剖开就是三个角色Binder实体binder_node、Binder代理binder_ref和ServiceManager。打个比方ServiceManager就是公司前台的总机系统服务是各办公室里的员工客户端进程是来办事的访客。每个服务在启动后会拿着门牌号服务名去总机那里登记客户端想办事先问总机“我要找谁”总机递给你一张访客通行证Binder代理你拿着通行证敲门里面的员工接电话办事执行transaction办完再通过电话线把结果传回来。这里员工本人就是Binder实体访客手里的通行证就是Binder代理通行的过程就是transaction。很多调试场景下你看到的“binder_ref”其实是代理对象而“binder_node”才是服务端真实的对象。比如log里出现“ref 1234”指的是某个进程持有的代理而“node 5678”是某个进程里的实名对象。搞不清这两个东西后面看日志会非常吃力。1.2 一次Binder调用从头到尾的路程要理解Binder的调试日志你必须能在脑回路里走一遍一次完整调用的路径。假设一个普通App进程调用系统的ActivityManagerService接口应用进程的Java层调用最终会走到ActivityManagerProxy的transact方法这个方法通过JNI进入libbinder的native层构造一个binder_transaction_data结构体然后通过ioctl写入/dev/binder设备节点。内核里的binder驱动收到这个ioctl后会处理这次transaction找到目标进程对应的Binder实体然后把transaction放到目标进程的binder线程队列里并唤醒目标线程去执行。目标执行完再通过同样的路径把返回数据写回来。就这么一条线大部分调试手段本质上都是在不同环节“拍照”观察/proc/binder/proc告诉你进程当前持有哪些对象、有哪些binder线程dumpsys binder告诉你用户态注册了哪些服务、各个服务状态如何strace能抓到ioctl那一层的系统调用。把这条路径了然于胸遇到问题你才能在第一时间判断“这次调用到底卡在了哪个环节”——是代理找不到实体是目标进程线程池满了还是数据量太大被内核拒了1.3 同步调用、oneway调用和binder线程池的边界Binder调用分两种同步sync和异步oneway。同步调用时调用方线程会阻塞等待服务端返回oneway调用则直接发出去就返回不会等结果。这个差异在调试中很重要如果你遇到主线程卡死八成是有个同步binder调用迟迟没返回如果你遇到“明明调用成功了但结果没生效”那可能是oneway调用产生了竞态。另外每个进程默认的Binder线程池大小是有限的。普通的App进程默认的Binder线程上限一般是16个系统进程如system_server会多一些但也不是无限。这个池子一旦被长时间占满所有新的跨进程调用都会排队等待表现出来就是接口超时、主线程ANR。后面第4章我会详细讲这个坑的排查方法。2. 最常用的四类调试手段实操详解2.1 直接看内核视角/proc/binder/系列节点Binder驱动本身在内核里维护了一套完整的调试信息通过procfs暴露出来。这是最直接、最底层、也是最少人知道怎么用的调试手段。常用的节点有这几个/proc/binder/proc/[pid]某个进程的Binder对象、线程、引用状态/proc/binder/transactions当前系统中所有活跃的transaction/proc/binder/stats全局的统计信息/proc/binder/state各线程的binder状态旧版本内核常见以查看某个进程为例命令是adb shell cat /proc/binder/proc/1234输出大致包含三块binder线程列表、该进程持有的Binder实体nodes、以及该进程持有的Binder代理refs。举例来说如果某进程的线程列表里出现一堆 “thread 985 0-16”说明这个进程已经起了17个线程如果这些线程长时间处于等待transaction的状态很可能就是池子被占死了。实际操作中我最常用这个节点来回答三个问题这个进程是不是binder线程耗尽了这个进程跟谁建立了binder连接谁持有了系统服务的关键代理尤其是第三个线上排查“服务未注册”“接口无效”之类的问题时看refs列表能非常直观地确认连接状态。注意/proc/binder/在Android 10以上的真机上可能需要root权限才能读取开发阶段用模拟器或者带root的debug固件会更方便。如果没法root可以退而求其次用dumpsys来获取一部分信息。2.2 用户态综合视图dumpsys binder和dumpsys servicedumpsys是Android系统提供的最强诊断命令之一Binder相关的有两块dumpsys binder和dumpsys service。dumpsys binder输出的是Binder驱动的用户态封装信息包括已注册的binder服务列表、每块binder存根节点、binder线程池的当前状态。输出量很大通常要配合grep过滤adb shell dumpsys binder --help # 查看可用参数 adb shell dumpsys binder # 直接看全局状态 adb shell dumpsys service --list # 查看系统已注册的服务名dumpsys service --list能列出所有注册到ServiceManager的服务。有时候第三方App会自己往ServiceManager里注册服务排查命名冲突或者服务注册不上的问题这条命令很有用。如果怀疑某个服务本身状态异常还可以单独dump某个服务比如adb shell dumpsys activity adb shell dumpsys window只要服务端实现了dump方法dumpsys的时候就能把服务里的状态打出来。这是个很有价值的信息源很多服务比如ActivityManagerService、PackageManagerService在dump里都会记录自己的binder事务统计和耗时。2.3 全链路跟踪用strace抓binder ioctl当你在用户态、dumpsys层面都看不出问题时就该往下沉一层用strace直接看进程有没有在做binder相关的系统调用。adb shell strace -f -p 1234 -e traceioctl -e ioctl0xc0186201这里的0xc0186201是binder ioctl的命令号不过不同内核和Android版本上数值可能不同更稳妥的做法是先strace所有ioctl再在里面grep binderadb shell strace -f -p 1234 -e traceioctl 21 | grep -E binder|BINDER你会看到类似这样的输出ioctl(14, BINDER_WRITE_READ, 0x7f...) 0只要进程有binder通信这里就会持续刷出BINDER_WRITE_READ等调用。strace在线上定位“某个进程是否还在跟系统服务通信”“调用是否卡在句柄读写”时非常有用。特别是你怀疑某个Native进程偷偷做了跨进程调用、或者某个binder调用阻塞了线程strace能直接把ioctl的进出时间直观地打出来——前后两条ioctl之间耗时异常就能判断这一层卡住了。注意strace在移动设备上需要root或debug权限对性能有一定影响生产环境慎用建议先在本地或测试环境复现。2.4 看日志dmesg和logcat里的binder蛛丝马迹Binder驱动在内核层的报错和警告会打到dmesg里。平时常见的有binder: undelivered TRANSACTION交易没送达、binder: 1234:1234 transaction failed 29189/-22返回码-22就是EINVAL一般是事务参数非法、以及binder: buffer not freed等。adb shell dmesg | grep -i binder | tail -n 100logcat层面Java/Native层有时候会打Binder相关异常。比如TransactionTooLargeException的堆栈、binder transaction失败时的ServiceSpecificException等。这些异常通常已经能提示你问题出在哪一步关键是你要能顺着堆栈找到发起者。我的习惯是遇到binder问题先把四类信息都采集一轮logcat的完整崩溃调用栈、dmesg中的binder片段、/proc/binder/proc对应进程的完整输出、以及dumpsys binder的快照。这四样东西对应了用户态调用方、内核驱动、进程对象状态、服务注册全局视图四个维度基本能定位90%以上的问题。3. 完整剖析一次Binder事务的来龙去脉3.1 动手构造一次可观测的Binder调用调试技巧光看理论记不牢最好是自己动手做一次“可观测的Binder调用”。我这里说一个小实验写一个简单的App绑定一个自己写的本地服务或者直接调用系统服务在调用前后分别抓取binder状态。比如我们写一个最小APP点击按钮的时候调用系统的SensorManager拿传感器列表。从logcat可以看到调用栈一路下去但想看底层的话先获取进程pid和binder线程信息# pid可以在开发者选项里看到或者用 pidof adb shell pidof com.example.bindertest adb shell cat /proc/binder/proc/pid此时抓到的结果里你会看到这个进程持有了若干个binder线程以及许多绑定系统服务的ref。点击按钮后再次抓取transaction相关的计数会增加线程状态会产生变化。通过前后对比你就知道这次调用消耗了哪个线程、用了多少时间。3.2 从内核日志里解读关键字段如果你想再往下挖可以在内核里打开Binder的动态调试。我用的比较多的是debugfs的dynamic_debug# 需要root echo file drivers/android/binder.c p /sys/kernel/debug/dynamic_debug/control echo file drivers/android/binder_alloc.c p /sys/kernel/debug/dynamic_debug/control配置好之后执行一次Binder调用再去看dmesg。你能看到类似这样的关键日志片段binder: 1234:5678 transaction 12345 from 9999:100 to 8888:100 node 6666里面的字段我可以简单解释一下最前面的1234:5678是当前线程的pid:tidtransaction后面跟的是这次事务的IDfrom是发送方的pid:tidto是目标进程的pid:tidnode后面的数字是目标进程里Binder实体的节点号。看到这行你就能确认这次调用的参与双方是谁。后续还能看到handle、code、flags等字段。code就是接口的transaction code可以用来判断调的是哪个接口flags里的0x01表示oneway调用。这些字段也许不常看但一旦需要深挖“到底是哪个接口在频繁调用”内核日志给你提供的就是最原始证据。3.3 从ANR/tombstone里提取Binder线索线上问题最常见的线索其实不是dmesg而是App挂在之后系统生成的ANR trace或者tombstone。ANR trace中每一个进程的Java线程和Native线程状态都会打印出来。需要关注两类线程一类是主线程。如果主线程的堆栈停在BinderProxy.transactNative说明主线程正在等待某个binder调用返回。这时候要往后看这个等待是因为调用太慢还是线程池满了排不上。另一类是binder线程池里的“binder:1234_5”线程。如果这些线程长时间停在transactNative或者waitForResponse上同时数量达到上限就能直接判定Binder线程耗尽。tombstone里同样有所有native线程的backtrace和状态。排查native层binder问题时我通常会先grep“binder_thread_read”“binder_ioctl”这些符号锁定线程最后停在内核的哪个函数上再结合上下文判断是排队等待还是死锁。4. 线上常见的Binder故障现象与排查实录4.1 Binder线程耗尽最经典的“timeout waiting for reply”这个故障在App侧的典型表现是主线程不卡但接口调用开始大面积超时logcat里有大量的“!!! FAILED BINDER TRANSACTION !!!”或者主线程卡死ANR trace里所有binder线程都在等transaction。根因通常是某个或某几个服务端响应过慢同时客户端不断发起同步binder调用把客户端进程的binder线程池全部占满。还有一种比较隐蔽的情况是服务端自身卡死导致所有客户端发过去的transaction都得不到回复从而波及整个进程池。排查步骤我一般这么走抓ANR trace或tombstone数一下“binder_”线程数量看是否到达16个线程上限。看这些binder线程停在哪如果全部卡在某个服务节点的transaction上基本明确是服务端sla差或死锁。用第2章的方法获取该进程的/proc/binder/proc确认是否有线程一直处于等待状态。如果服务端是系统服务还要去看对应系统服务的dumpsys输出比如AMS卡死就dumpsys activity。修复方向通常有两个一是优化服务端响应时间比如把重活从同步binder callback里挪出去二是在客户端做限流和超时控制避免无脑重试把线程池打爆。真到了生产环境给调用端做熔断往往比优化服务端更快止血。4.2 TransactionTooLargeException事务太大这个异常在开发阶段其实也经常遇到往Intent里塞大图片、往Bundle里塞大数据、或者一次查询返回了超大列表导致Binder传参超限。Binder的事务缓冲区默认上限约1MB但Java层的Parcel在1MB以下就可能报错因为还需要预留一些头部空间。这个限制是内核驱动层面的硬约束不是你程序里改个参数能解决的。遇到了就做三件事一是把大对象改成文件句柄或者content://URI传过去这样就只传路径不传数据二是改用分页查询把列表数据切碎分批传输三是大图片先压缩再传。这三个方案都能回避1MB的缓冲区限制。另外提一个容易被忽略的点RemoteCallback这类回调场景也会消耗binder缓冲。某些系统方法内部会在binder上传递数据即使你主观上觉得自己没传大数据也可能因为回调参数集合太大触发异常。排查时建议把binder相关的异常栈整体截图先看是哪个接口触发的再沿着调用链去查是谁往parcel里写了大量内容。4.3 死锁与主线程卡死跨进程锁顺序问题Binder死锁有一种典型模式进程A持有本地锁然后跨进程调用进程B的服务进程B拿到这个请求后又反向调用进程A的服务。如果这组调用互相等待就形成了跨进程死锁。此时两个进程各自的本地锁和各自的binder线程互相卡住日志层面极易出现一条链上的所有操作全部timeout的现象。还有一种更隐蔽的同进程死锁主线程通过binder调用同一个进程内另一个binder服务时如果这个服务端实现也在等主线程的另一个操作就会导致主线程自己等自己实际上由于线程池的存在程内跨线程调用也可能出现这种互相等待。这类问题用传统锁排查一般找不到因为锁是“合理的”卡住的是binder线程池。排查Binder死锁我推荐一个思路同时抓取所有相关进程的线程栈注意是同时抓完一个再抓另一个就晚了然后用“一个进程的binder线程在等另一个进程的线程在等另一个进程的binder线程”这样的链条去对。线上一旦确认某次调用是这种互相等待最好的解法就是改业务架构别把跨进程调用放在锁的临界区里或者把其中一端的同步调用改成oneway异步回调。4.4 oneway调用带来的隐性问题oneway调用不等待结果这在设计上是给性能解绑但也埋了不少坑。最常见的坑有两类一是乱序——多个oneway调用并不保证按发送顺序执行如果业务上对顺序有要求就会出问题二是“悄然失败”——oneway调用发出后返回值直接是0如果服务端已经挂了调用方根本感知不到。调试这类问题时日志里的transaction flags 0x01是重要线索。如果发现某条操作偶尔成功、偶尔失败但没有任何同步异常先检查调用是不是oneway。另外服务内部的回调机制也经常和oneway组合出问题注册了一个回调结果回调在服务端被静默丢弃客户端没有任何反馈。这时候建议在客户端和服务端各打一条日志对比服务端是否收到请求、客户端是否收到回调很快就能定位。4.5 DeathRecipient与远程服务掉线的隐藏坑Binder代理在服务端进程死亡后会自动变成“死亡状态”。如果客户端没有注册DeathRecipient访问时只会收到一个偶尔成功、偶尔异常的服务调用注册了DeathRecipient的话会在服务端死亡时收到binderDied回调。但DeathRecipient也不是万能护身符。实战里调试过一个很奇怪的问题客户端已经注册了DeathRecipient服务端进程也没死但回调就是不触发。最后发现是因为服务端进程是系统级服务虽然进程还活着但是某个binder实体被重新创建了比如服务内部做了一次软重启或者把binder节点替换了旧的代理没有收到死亡通知但已经指向一个失效的实体调用就石沉大海。遇到这类问题就是典型的“代理对象与实体对象失配”。排查时看/proc/binder/proc里的ref和node对应关系最直接如果ref指向的node已经不存在那就说明代理失效了。面对这种情况好的做法是在客户端增加调用失败后的重绑定机制而不是依赖DeathRecipient这一条单一路径。5. 关于Binder调试工具选型和一些排障心得5.1 工具对比不同场景选什么刚才介绍了/proc节点、dumpsys、strace、内核日志四种手段再加上常见的logcat和ANR trace很多人会纠结到底该先用哪个。我个人的判断标准是看问题发生的层面问题是开发阶段偶发优先logcat ANR trace成本低、信息直观。问题发生在真机线上优先tombstone dmesg先把堆栈和内核日志导出来。问题疑似服务端状态异常优先dumpsys system_server相关的dump输出。问题怀疑是特定进程的binder持有关系混乱优先/proc/binder/proc。问题涉及时序和性能优先strace 内核动态日志。需要定位到底是哪个接口在被高频调用配合内核binder动态打印再结合业务日志做交叉确认。这些工具之间是互补关系不是替代关系。排查疑难问题时我基本上会把能拿到的信息全部拉齐再做交叉比对只依赖单一数据源容易得出错误结论。5.2 线上采集日志的一些小技巧线上问题最怕的就是用户设备上没抓取到足够信息。我和团队现在在外面跑的三方设备上都会预设一个“性能诊断开关”平时只打少量日志一旦检测到某个进程出现主线程长时间卡顿或者binder调用异常就自动把当前时刻和前后30秒的logcat、dmesg、所有相关进程的线程栈、/proc/binder/proc快照一起落盘。这个快照机制救过我们很多次很多现场没有复现条件的问题靠着这些落盘数据一样能还原出全貌。另外写自定义系统服务或者重系统组件时我强烈建议在服务端拦截层统一做一个binder调用的耗时统计埋点。这个埋点数据的价值会越积越大你会发现某个接口操作基本盘是2ms某次突然跳到300ms那这300ms就是你下一次排查的最好切入点。5.3 我自己踩过的几个坑的总结最后分享几个我用真金白银换回来的经验教训。第一个教训不要把Binder的ANR问题当成卡顿去优化系统性阻塞如果不从线程池和调用链入手单纯优化单个接口的耗时是治标不治本。第二个教训排查跨进程问题的时候最忌讳只盯着某一个进程看客户端和服务端要一起抓线程栈要尽量在同一个时间点采集否则时间差超过几百毫秒你已经错过了关键现场。第三个教训如果是三方SDK引起的Binder占用先别急着骂把证据链做齐——什么接口、什么时间点、哪个线程、持续多久——然后带着证据找SDK厂商这样效率最高。Binder调试本质上是一场“还原现场”的侦探游戏。它不像普通Java异常那样看一眼堆栈就能定位而是要从进程、线程、内核、驱动多个维度交叉验证。把这一整套方法内化之后你会发现Binder相关问题的排查难度会直线下降。希望这篇文章的总结能帮你少走我当初走过的弯路。
返回列表