第一章:内核模块基础

各位同学,欢迎来到《手机SoC神经网络加速器驱动开发指南》的第一章。

做NPU驱动,说白了就是跟Linux内核打交道。你想想看,一个神经网络加速器要跑起来,首先得让内核认识它、管理它。怎么做到?靠的就是内核模块。

我个人习惯把内核模块比作「乐高积木」。内核是那个大底板,模块就是一块块积木。你需要什么功能,就插什么模块。不需要了,拔掉就行。不用重新编译整个内核,也不用重启机器。嗯,这个设计思路,真的很妙。

核心要点:内核模块是Linux内核动态扩展能力的基石。在NPU驱动开发中,我们几乎所有的加速器控制逻辑,都会以内核模块的形式存在。

1.1 内核模块框架

一个最简单的内核模块,长什么样?我直接给你看代码:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init my_npu_init(void)
{
    printk(KERN_INFO "NPU加速器模块加载成功\n");
    return 0;
}

static void __exit my_npu_exit(void)
{
    printk(KERN_INFO "NPU加速器模块卸载成功\n");
}

module_init(my_npu_init);
module_exit(my_npu_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("NPU加速器驱动模块示例");

这段代码,是所有NPU驱动的起点。我当年第一次写NPU驱动时,就是从这么个空壳子开始的。

注意看几个关键点:

  • module_initmodule_exit:这是内核模块的入口和出口。加载时调用init函数,卸载时调用exit函数。
  • __init__exit:这两个宏是给内核的提示。__init标记的函数,加载完后内核会把这段内存释放掉。省内存的小技巧。
  • MODULE_LICENSE:必须声明许可证。不写的话,内核会报「tainted kernel」的警告。我在项目中遇到过,有些芯片厂商的闭源驱动用了非GPL许可证,结果内核社区的人都不愿意帮他们看问题。

我的习惯:每个模块文件开头,我都会写清楚MODULE_DESCRIPTION。半年后回头看代码,你会感谢自己写了这行注释。

1.2 模块加载与卸载

写好了模块代码,怎么让它跑起来?

编译:

make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

加载:

sudo insmod my_npu.ko

查看:

lsmod | grep my_npu

卸载:

sudo rmmod my_npu

这里有个坑,我曾经踩过。insmod加载模块时,如果依赖其他模块,你得先加载依赖。NPU驱动通常依赖几个基础模块,比如内存管理、中断控制器。顺序搞反了,加载就会失败。

注意:rmmod卸载模块时,如果模块正被其他进程使用,卸载会失败。NPU驱动里,如果推理任务还没跑完,你强行卸载,系统可能会崩溃。我建议在卸载前,先检查一下设备文件是否还在被占用。

为什么会这样?因为内核模块的生命周期,跟用户空间的进程是绑定的。你想想看,NPU正在算一个模型,你突然把驱动拔了,硬件寄存器全失效了,内核不panic才怪。

1.3 printk调试技巧

做驱动开发,调试手段很有限。没有gdb,没有断点。你靠什么?靠printk。

printk是内核版的printf。用法差不多,但有几个关键区别:

日志级别 宏定义 使用场景
KERN_EMERG 0 系统崩溃前,紧急信息
KERN_ALERT 1 需要立即处理
KERN_CRIT 2 严重错误
KERN_ERR 3 一般错误
KERN_WARNING 4 警告信息
KERN_NOTICE 5 正常但重要的信息
KERN_INFO 6 信息性消息
KERN_DEBUG 7 调试信息

我个人习惯,调试NPU寄存器读写时,用KERN_INFO。排查bug时,用KERN_DEBUG。生产环境里,只留KERN_ERR以上的日志。不然dmesg会被刷爆。

看日志的命令:

dmesg | tail -20

或者实时监控:

tail -f /var/log/kern.log

避坑指南:我曾经在NPU中断处理函数里用了printk。结果一触发中断,printk打印,控制台输出,中断又被触发...死循环了。记住,中断上下文里别用printk,除非你想看系统卡死。

printk还有个好处,可以加格式控制。比如打印寄存器地址:

printk(KERN_INFO "NPU reg 0x%08x = 0x%08x\n", reg_addr, reg_val);

嗯,这里要注意。内核里不能用%llx打印64位值?其实可以,用%llx就行。但有些老内核不支持,我建议统一用%pad打印物理地址,用%pap打印指针。

1.4 本章知识体系

为了让你更直观地理解本章内容,我画了一张图:

内核模块知识体系 模块框架 加载与卸载 printk调试 module_init / module_exit MODULE_LICENSE / 作者信息 insmod / rmmod / lsmod 依赖顺序 / 资源释放 日志级别 / dmesg查看 中断上下文禁用printk 核心:模块是NPU驱动的载体,printk是调试的拐杖 加载卸载要谨慎,调试日志分级别

这张图把本章的三个核心知识点串起来了。模块框架是骨架,加载卸载是动作,printk是眼睛。三者缺一不可。

补充一句:实际项目中,NPU驱动模块往往不止一个文件。我习惯把硬件操作放一个文件,把上层接口放另一个文件。模块化设计,后期维护会轻松很多。

好了,第一章就到这里。记住,内核模块是你和NPU硬件之间的桥梁。桥搭得稳不稳,就看你这模块写得扎不扎实。


公众号:蓝海资料掘金营,微信deep3321