当前位置:首页>Linux>一个ioctl引发的血案——嵌入式Linux应用开发避坑手记

一个ioctl引发的血案——嵌入式Linux应用开发避坑手记

  • 2026-09-02 17:01:01
一个ioctl引发的血案——嵌入式Linux应用开发避坑手记

半夜两点客户甩来kernel panic截图,一台i.MX6ULL工业平板在压力测试中崩溃。从应用层的read调用查到内核evdev驱动的竞态条件,最后发现坑在input事件处理路径里。

一个ioctl引发的血案——嵌入式Linux应用开发避坑手记

有次半夜两点,客户突然甩过来一张截图,说设备跑了八个小时之后触摸屏完全没反应了。我看了一眼log,kernel panic。当时后背一凉——这玩意儿第二天早上要拿去展会演示。

搞嵌入式Linux的应该都懂,应用层出问题还好说,大不了重启进程。内核崩了,那就真的"死给你看"了。

先说一下背景。我们当时做的是一款基于i.MX6ULL的工业平板,跑Linux 4.19,QT5.12做界面,通过SPI接了一个触摸控制器。驱动芯片是现成的,原厂给了源码,我们只是改了一下设备树里的引脚配置。测试了三天,功能一切正常,于是交付小批量给客户试产。

结果客户那边跑了不到一宿就挂了。

刚开始死活复现不了。我们自己在实验室连续跑了两天都没出问题。后来看了客户的运行日志,发现一个规律:每次崩溃之前都伴随着大量触控操作——快速滑动、多点触摸交替进行。频率大概是一分钟内操作几十次。

这就有点意思了。

我一开始怀疑是SPI驱动的问题,毕竟i.MX系列的SPI控制器有过一些已知的errata。但仔细翻了数据手册,发现我们的操作频率远低于触发条件。而且如果真是硬件Bug,应该更早暴露,不至于跑八个小时才出问题。

那就只能从应用层往下查了。

我们的触摸事件处理,用的是input子系统。应用层通过open("/dev/input/eventX", O_RDONLY)拿到fd,然后在一个while循环里反复调用read。这是最常规的做法,没什么花活。但问题就出在这个"常规做法"上。

看代码:

while (1) {
    struct input_event ev;
    int ret = read(fd, &ev, sizeof(ev));
    if (ret < 0) {
        perror("read error");
        break;
    }
    // 处理触摸事件...
    handle_touch(&ev);
}

一眼看过去没啥毛病对吧?我也是这么想的。但仔细一想——如果用户连续快速触摸,事件队列里的数据积压,read是会一直有数据返回的。那么while循环里handle_touch如果处理得慢,会不会堵?

查了一下handle_touch的实现,里面居然有个同步的socket通信,去向一个远程服务查询当前界面状态。网络如果抖一下,这个调用可能阻塞几十毫秒甚至更久。问题是input事件不等人啊——事件缓冲区满了之后,内核的evdev驱动会触发一个reset操作,把整个input设备的状态清空,紧接着释放缓冲区。

释放缓冲区的时候如果应用层正在read,或者在read返回后内核还没来得及把数据完全copy到用户空间,就可能会触发一个野指针访问。

我之前一直以为input子系统的evdev很稳,毕竟Linux内核的输入框架从2.6时代就在用了,经历了这么多年打磨,应该不会有这种低级问题。但看了内核源码(drivers/input/evdev.c)才发现,evdev_events和evdev_read之间的竞态条件其实一直存在,只是触发条件比较苛刻。

修复方式倒不复杂。

方案一是把handle_touch里的阻塞操作去掉,改为异步处理。我们重构了那部分逻辑,把socket通信丢到独立线程里,handle_touch只负责把事件塞到一个环形缓冲区就返回。这样read就能以最快速度把事件消费掉,不会积压。

方案二是用epoll而非阻塞read,这样可以在事件到达和其他fd之间做多路复用,万一某个事件处理卡了,不会影响整体的读循环。

int epfd = epoll_create1(0);
struct epoll_event ev, events[8];
ev.events = EPOLLIN;
ev.data.fd = input_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, input_fd, &ev);

while (1) {
    int nfds = epoll_wait(epfd, events, 8, 500);
    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == input_fd) {
            struct input_event ev_buf[8];
            int n = read(input_fd, ev_buf, sizeof(ev_buf));
            for (int j = 0; j < n / sizeof(struct input_event); j++) {
                async_enqueue(&ev_buf[j]);
            }
        }
    }
}

两个方案最后我们都上了。改了之后让客户再跑压力测试,连续跑了三天三夜都没出问题。说起来也惭愧,一个如此常规的read调用,居然能踩到内核的暗坑。

事后复盘,我更倾向于认为这不是evdev的"Bug",而是我们应用的读模式踩到了一个不太常见的路径。毕竟input子系统主要面向桌面环境,桌面环境下触摸事件的频率远没有工业平板这么高,而且桌面应用一般也不会在handle_touch里做同步网络调用。

所以吃一堑长一智吧。

最后说几个结论性的东西建议你记住:(1)别在input事件处理路径里做任何阻塞操作,这玩意儿比你想的脆弱;(2)能用epoll就别用阻塞read,多路复用在嵌入式里不是摆设;(3)如果你用的芯片比较老或者内核版本比较旧,多翻翻drivers/input/evdev.c,说不定你的下一个Bug就在那等着你。

那台i.MX6ULL的板子现在还在我们实验室跑着,显示器上挂着压力测试的log。到今天已经是第47天了,没崩过。不过谁知道呢,也许第48天它就又挂了——嵌入式嘛,永远别觉得稳了。

最新文章

随机文章