跳到主要内容

将用户态点灯程序搬到内核态的关键流程与其中的坑

前言

为了定位某个死机问题,需要将运行在用户态的 led 点灯程序搬到 linux 内核态运行。

用户态 led 点灯程序分析

用户态 led 点灯程序分为两个层次,第一层是 led 驱动 api ,第二层是调用 led 驱动 api 的程序。

led 驱动 api 的关键流程如下: 在这里插入图片描述 上述流程通过 mmap /dev/mem 来将 led 的物理地址映射为进程的虚拟地址访问,映射成功后首先配置引脚的复用功能,然后根据上层传入的 led 控制命令来配置寄存器。

基础命令包括:

  1. led 亮
  2. led 灭

上层程序通过周期性的切换 led 的亮灭状态来达到预期的点灯状态。

从用户态到内核态迁移的关键过程

对用户态的点灯程序的工作原理进行分析,能够识别出如下几个关键问题:

  1. 如何映射 led 灯的物理地址?
  2. 内核态如何周期性的切换 led 灯的状态?

初版实现

在这里插入图片描述 初版的实现如上图所示。

加载点灯模块时,通过 ioremap 来映射 gpio 物理地址,成功后调用 kthread_run创建并运行一个内核线程来周期性点灯。

卸载点灯模块时,首先通过 iounmap 解除 gpio 地址映射,然后注销创建的内核线程。

测试发现加载模块正常,卸载模块时内核 crash,根据崩溃信息排查发现问题点是内核态点灯线程访问映射出的 gpio 地址时崩溃。

由于以前遇到过类似的问题,我马上反应过来,应该是内核态点灯线程停止前就执行了 iounmap 导致点灯线程访问了非法地址

检查上述逻辑,确实存在这个问题。

改进版本

在这里插入图片描述 在改进版本中,led 点灯线程运行时会不断判断是否要 stop,当卸载模块时,模块解初始化函数调用 kthread_stop 向点灯线程发信号,点灯线程接收到这个信号后执行 iounmap 释放映射的 gpio 地址然后退出。

点灯线程的主要代码如下:

static int led_shink_thread(void *unused)
{
..................................

while(!kthread_should_stop()) {

...................
schedule_timeout(1 * HZ);
}

iounmap(gpio_base_addr);

return 0;
}

schedule_timeout 函数让内核线程周期性地让出 cpu,频率为 1s 一次。

为什么不调整初版中关闭 led 线程与 iounmap 的顺序?

初版的问题点是在内核态点灯线程停止前就执行了 iounmap 导致点灯线程访问了非法地址,头脑中的第一个想法可能是将逻辑修改为先停止点灯线程再 iounmap,那为什么不这样改呢?

其实这个简单的想法忽略了一个关键的问题: rmmod 的进程进入内核执行 kthread_stop 终止内核线程时,kthread_stop 执行完成并【不代表】内核线程已经退出。

这样在 led 点灯线程真正终止与 iounmap 执行成功就存在竞争。如果 iounmap 先执行完,而 led 点灯线程还在访问映射出来的 gpio 地址,就会导致内核 crash。

总结

这一任务是在半年前做的,记得当时在写完后第一次加载内核模块时内核就崩溃了,但是只记录了内核 oops 信息却没有记录出问题的点,现在已经没有一点印象了,这一点需要加强。

表面上看这个任务挺简单的,真正做起来的时候还是踩了一些坑,这些坑背后其实都有一些实际存在的问题,意识到这些问题并深入思考推动解决后觉得很有些收获。

本文也分析了遇到问题时大脑里最先跳出来的解决方案,看似合理其实却不可行,这就是系统一在作怪了,对这种情况还是要保持谨慎并仔细分析,发挥系统二的思考能力,这样才不致于落入系统一的陷阱中。