这是一个内核模块开发的备忘,主要是中断处理中的 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 中,调用阻塞接口也没有关系。

改动:

1
2
3
4
5
6
7
8
# 新增结构
struct work_struct wake_work
# 初始化
INIT_WORK(...)
# irq 调用
schedule_work(&ctx->wake_work)
# 销毁
cancel_work_sync(...)

然后测试,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 的最差开销加上两次切换,那么的确会恶化延迟。

继续修改:

1
2
3
4
5
6
7
8
# 新增结构
struct tasklet_struct wake_tasklet
# 初始化
tasklet_init(...)
# irq 调用
tasklet_schedule(&ctx->wake_tasklet)
# 销毁
tasklet_kill(...)

继续测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Trigger 数据:                Event 数据:
11687: 1781254159 000002905  | 0 11687: 1781254159 000013405
11688: 1781254159 200002550  | 0 11688: 1781254159 200009550
11689: 1781254159 400004237  | 0 11689: 1781254159 400014153
11690: 1781254159 600004173  | 0 11690: 1781254159 600011465
11691: 1781254159 800002943  | 0 11691: 1781254159 800009652
11692: 1781254160 000003172  | 0 11692: 1781254160 000009589
11693: 1781254160 200003984  | 0 11693: 1781254160 200010400
11694: 1781254160 400003629  | 0 11694: 1781254160 400010337
11695: 1781254160 600003565  | 0 11695: 1781254160 600009690
11696: 1781254160 800003794  | 0 11696: 1781254160 800011377
11697: 1781254161 000003439  | 0 11697: 1781254161 000013939
11698: 1781254161 200003667  | 0 11698: 1781254161 200011834
11699: 1781254161 400005646  | 0 11699: 1781254161 400013521
11700: 1781254161 600003249  | 0 11700: 1781254161 600009666
11701: 1781254161 800004644  | 0 11701: 1781254161 800012811

总体上看,延迟要好很多,平均在 10 us 以内了,也符合预期。

总体分析,因为 hrtimer HARD 模式下,回调会禁用 irq ,那么整体会串行(同一个 cpu 下),前者处理时间越长,那么后者延迟就越大。 第一次分析出了部分原因,但是使用了不合适的 BH 处理框架。

虽然内核现在开始冻结 tasklet 的使用,并且提供了 BH workqueue 来代替(kernel 6.9+),但是可见很长时间以内,tasklet 还是可以可靠使用的,尤其针对这种时间敏感型模块。