IRQ 中 BH 处理的选择
Contents
这是一个内核模块开发的备忘,主要是中断处理中的 Botto-Half 处理框架选择有关。
(本文均为古法手搓,没有 AI 加成,如有错字语病实属正常)
今天在调试之前时间同步相关的内核模块时,连接一个基于 realtime 触发的 Trigger 模块到一个基于 gpio 输入的 Event 模块(通过 GPIOs 连接)时,Event 的时间戳相对 Trigger 的时间戳有较大延迟,平均在 20 微秒,理论上在这个 RK3506 平台上不应该有这么大的延迟,应该在 10 us 以内(GPIO 3-4us 加上内核调度延迟)。
那只能继续细看并分析了。
现有的内核模块时这样处理的,在 hrtimer / irq 回调中,会将时间戳和数据打包到 kfifo 中,然后通知可能阻塞等待新
数据的用户进程(用户空间poll),这里用到了 wake_up_interruptible(),实测发现当真的有程序在等待时,这个调用有接近10us 的开销,但是没有程序等待时,则开销很低。
但是使用这个模块必然需要程序来监听读取数据,那么简单,把这个调用放到 Bottom-Half 就行了。因为 wake_up 没有
实时性要求,所以直接选择了 workqueue 来做,workqueue 工作在 process context 下,实质在 kworker 中,调用阻塞接口也没有关系。
改动:
|
|
然后测试,Event 与 Trigger 延迟基本降低在 12 us 左右,有改善。但是多次测试下来发现,有时候延迟还是会上升到 20+ us ,而且一旦在这个级别,延迟会稳定在这个级别。
这就有点奇怪了,在负载变化不变的情况下,不应该是这个现象。既然好复现,就直接输出调试时间差。
结果发现,Trigger 的 irq 全程差不多要 10-21 us 左右,而且基本上都超过 10us 了,这不符合预期。因为实际硬件上
Trigger 使用的 HARD hrtimer 模式,也就是在回调中 irq 会被禁用,而且 Event 的 irq 与 Trigger 在同一个 CPU 上,那么这时候 irq 就是串行的,意味着 Trigger 处理耗时,那么 Event 的延迟必然增大。而且这里有时候延迟 20+ 与
直接在 irq 中调用 wake_up 就很相似了。
继续测量时间,发现 Trigger 的回调中,schedule_work 的耗时很不稳定,间隔的忽高忽低,高可以到 10+ us(含调试代码),低的时候只有几百 ns。这也与预期不符合,直觉说明 schedule_work 有隐藏开销。
schedule_work 底层实现上,因为依赖内核进程,那么需要通知/激活对应的进程,如果进程在休眠,则还需要调度唤醒,这里面就会有开销。这跟 tasklet
很不一样,tasklet 基于 softirq 实现,tasklet_schedule 只需要标记下,后续 softirq 会直接执行对应任务,要求是任务里不能阻塞而已。
从这一点分析,在 Trigger 回调之后,唤醒的 kworker 被调度运行,这时 Event irq 触发,那么又需要切换上下文进入中断。本身 schedule_work 的最差开销加上两次切换,那么的确会恶化延迟。
继续修改:
|
|
继续测试:
|
|
总体上看,延迟要好很多,平均在 10 us 以内了,也符合预期。
总体分析,因为 hrtimer HARD 模式下,回调会禁用 irq ,那么整体会串行(同一个 cpu 下),前者处理时间越长,那么后者延迟就越大。 第一次分析出了部分原因,但是使用了不合适的 BH 处理框架。
虽然内核现在开始冻结 tasklet 的使用,并且提供了 BH workqueue 来代替(kernel 6.9+),但是可见很长时间以内,tasklet
还是可以可靠使用的,尤其针对这种时间敏感型模块。