之前遇到这样一个bug:在一个性能较差的linux平台上的一个Qt程序,当UI线程在执行耗时操作时,界面会卡顿,而这时频繁点击滑动鼠标,会出现鼠标事件丢失的问题。举个例子:某个控件收到一个鼠标按下的事件,但再也没有收到鼠标弹起事件,而此时鼠标按键实际上已经弹起了,这就导致程序进入了一种异常状态,除非再次点击鼠标,否则无法恢复。上面这个问题是在QApplication的事件过滤器中确定的,全局的QApplication事件过滤器能记录下所有的Qt事件。
那么如何确定事件是在哪里丢失的呢?首先要大概的了解一下Qt鼠标事件的生成与分发过程,由于上述平台没有窗口管理系统(一般是Xorg),所以大致流程是这样的:
- 鼠标产生动作
- 内核响应中断,将鼠标事件放入evdev缓冲区
- Qt从evdev缓冲区中读取鼠标事件
- Qt分发鼠标事件
- Qt处理鼠标事件
那么事件是在哪一步丢失的呢?经过分析,第3步是由QEvdevMouseHandler::readMouseData函数完成的,通过记录日志发现,鼠标弹起事件在这一步就已经不见了。而这一步呢,是Qt能接触到鼠标事件的第一现场,也就是说问题的直接原因并不在于Qt,而在于内核。
回过头来看一下,上面的1-5步看起来像是串行的,实际上并不是:内核的中断响应与Qt的事件读取是两个独立的流程,1-2是一个内核的循环,3-5是一个Qt的循环,而这两个循环共享了同一个evdev缓冲区。如果内核的循环跑的很快,而Qt的循环由于什么原因被卡住而跑的很慢时会发生什么呢?自然evdev缓冲区会发生溢出。以我手上的4.16.0版本的内核源码为例,其中evdev.c中的__pass_event函数用来向evdev缓冲区中压入事件:
static void __pass_event(struct evdev_client *client,
const struct input_event *event)
{
client->buffer[client->head++] = *event;
client->head &= client->bufsize - 1;
if (unlikely(client->head == client->tail)) {
/*
* This effectively "drops" all unconsumed events, leaving
* EV_SYN/SYN_DROPPED plus the newest event in the queue.
*/
client->tail = (client->head - 2) & (client->bufsize - 1);
client->buffer[client->tail].input_event_sec =
event->input_event_sec;
client->buffer[client->tail].input_event_usec =
event->input_event_usec;
client->buffer[client->tail].type = EV_SYN;
client->buffer[client->tail].code = SYN_DROPPED;
client->buffer[client->tail].value = 0;
client->packet_head = client->tail;
}
if (event->type == EV_SYN && event->code == SYN_REPORT) {
client->packet_head = client->head;
kill_fasync(&client->fasync, SIGIO, POLL_IN);
}
}
可以看出来evdev缓冲区是一个环状缓冲区,如果插入事件导致head与tail相等,即可判定缓冲区发生了溢出,会丢弃缓冲区中的所有事件,同时内核会向缓冲区放入一个SYN_DROPPED事件,通知用户程序事件有丢失。不巧的是,Qt的QEvdevMouseHandler::readMouseData对这个事件没有做任何处理,也就无法自动从异常状态恢复了。如果希望解决这个问题,可以考虑从这个特殊的SYN_DROPPED事件下手。
PS:虽然事件是被内核丢掉的,但Qt和用户程序对此也要负一半责任:为什么Qt要把事件读取和事件响应放在一个UI线程里面去跑呢?用户代码为什么要把耗时操作直接在UI线程中跑呢?
PPS:如果事件源源不断产生,而响应事件平摊耗时总是大于生成事件耗时,不管缓冲区有多大,总有一天会溢出的,不过这种极端情况似乎并不容易出现。