CTS/GTS问题分析9 | weiinter105

一个由kernel层驱动错误导致的错误,查明的过程比较曲折,因此记录一下

问题初探

测试命令: run cts -m CtsHardwareTestCases -t android.hardware.input.cts.tests.GamepadTestCase#testButtonA

报错如下:


 

测试case code如下:

 

那么看出流程大致如下: 首先注册一个名为Odie (Test)的hid device,首先发送一个down事件,延迟10ms,检查测试界面是否接收到了down事件;然后再传递一个up事件,然后检查测试界面是否接收到了up事件

case栈报错的原因是应该接收到up事件时接收到的还是down事件,因此case报错

问题分析

(a)分析过程1:
首先直觉怀疑是不是case的时间设置问题导致发送的down事件被测试页面当作了长按,这个验证很简单,简单修改下case

 

异常F1:

 

正常F5:

 

可见,up事件在case运行过程中并没有被正常接收到,直到case fail,测试页面InputCtsActivity pause才发出了up事件

 

(b)分析过程2:
上面说到up事件没有被正常接收到,首先看一下底层驱动是否有正常上传up事件,首先在input event从内核上传的必经之路input_event加一些log

 

相关log:

 

最后发现,并没有up input event上传的迹象,而down事件是正常上传的,那么传递event事件流程中出问题的概率就很小了,那么首先怀疑up事件是否就根本没有从用户空间传下去,那么先看传输流程

case是通过hid command命令进行事件传输的,我们找到相关位置http://guard.pt.miui.com/opengrok/xref/v10-p-cepheus-dev/frameworks/base/cmds/hid/src/com/android/commands/hid/Hid.java#46

整理关键代码如下:

 

frameworks/base/cmds/hid/src/com/android/commands/hid/Device.java

 

frameworks/base/cmds/hid/jni/com_android_commands_hid_Device.cpp

 

(c)分析过程3:
首先如果要继续分析,需要对Handler有个大致的了解,简单来说就是一个线程向另一个持有looper的线程发送消息,持有looper的线程通过loop()循环选取Message进行处理

那么只有两种可能: 1.消息没入队 2.入队没处理

首先看消息没入队的情况,本例中是通过sendMessageAtTime来向队列中投递消息的,大体流程

frameworks/base/core/java/android/os/Handler.java

 

frameworks/base/core/java/android/os/MessageQueue.java

 

我们注意到enqueueMessage是会返回是否入队成功的,那么问题就很简单了,在case里把是否入队成功打出来就行了,结果发现入队是成功的;那么就剩下第二种可能,消息虽然在队中,但没有被取出来执行,这里的流程在http://guard.pt.miui.com/opengrok/xref/v10-p-cepheus-dev/frameworks/base/core/java/android/os/Looper.java#137

 

那么肯定是卡在Message msg = queue.next(); // might block ,这也解释了为什么测试报错之后会卡住不生成log的原因,直到触发了CTS框架的整体超时机制,那么下面看一下MessageQueue.next()的操作

 

因为case卡住了,那么很方便我们抓trace,先判断卡在哪里,发现果然卡在nativePollOnce

 

队列为空时timeout为-1,会一直等待,但是等到下一个Message入队时就应该被激活啊,而我们前面已经验证了up Message是入队的,因此这里就矛盾了,所以肯定还有什么我们没考虑的地方

先看下nativePollOnce的等待流程

frameworks/base/core/jni/android_os_MessageQueue.cpp

 

/system/core/libutils/Looper.cpp

 

 

Looper初始化时会初始化一个fd,**mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); **

前面说道,当一个队列中没有Message时,会陷入无限等待,当新消息入队时,我们将MessageQueue重新唤醒:

enqueueMessage中

 

 

 

通过epoll机制自动唤醒mWakeEventFd,那么没有被唤醒说明needWake为false,如果MessageQueue为空,这个值只能根据当前是否block来决定,那么必然会wake,只能怀疑Messagequeue不为空

在根据这里,强烈怀疑设置了一个屏障Message导致queue没有被wakeup

再看case:

 

处理Message (MSG_OPEN_DEVICE)设置了屏障,在DeviceCallback中的onDeviceOpen被消除,如果这个没有回调,就会发生这个up消息入队却无法执行的现象,case中加log发现果然没有被回调。那么问题又回到底层,需要找出Callback没有被回调的原因。

(d)分析过程4:
首先看这个callback被调用的位置

 

在这里add_Fd,然后在pollInner中进行处理

 

可见onDeviceOpen这个callback的回调与这个event UHID_OPEN息息相关,可见是没有从内核上传UHID_OPEN上来,我们看一下uhid.txt中的说明:

 

The first thing you should do is sending an UHID_CREATE2 event. This will register the device. UHID will respond with an UHID_START event.

从用户态发送UHID_CREATE2到内核态(UHID),然后内核态(UHID)返回UHID_START和UHID_OPEN到用户态

然后通过添加log,大致理清楚了流程

首先在http://guard.pt.miui.com/opengrok/xref/v10-p-dipper-dev/kernel/msm-4.9/drivers/hid/uhid.c#642中初始化工作队列:

 

然后从用户态向内核态发送event UHID_CREATE2

 

然后内核态监听到这个event后,开始uhid_dev_create2创建操作

 

注意这里schedule_work(&uhid->worker); 将工作结构变量添加到系统共享队列,等待执行,其工作处理函数为uhid_device_add_worker,在其中发送UHID_START和UHID_OPEN,大体流程如下:

 

 

然后排查这一段逻辑:

 

最终发现错误在http://guard.pt.miui.com/opengrok/xref/v10-p-cepheus-dev/kernel/msm-4.14/drivers/hid/hid-core.c?r=aaac082d#1804

<

 

注意

 

而调用的driver->open里面

 

 

hid->ll_open_count这个值加了两次,导致无法进入retval = uhid_queue_event(uhid, UHID_OPEN),UHID_OPEN这个event无法向上传递,最终没有调用到定义的callback,导致messagequeue被阻塞,up消息无法发送,case fail,本地修改hid_hw_open为:

 

再编bootimage刷一下,就可以通过测试了

问题总结

这个问题是kernel更新之后里面的hid driver实现逻辑有问题造成的,中间回顾了Handler实现逻辑,用到了linux的epoll,workqueue机制(只有基本了解),有待后续进一步详细了解。

作者: RESSRC

个人资源站

发表评论

邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据