0%

【驱动开发】Linux 驱动开发入门(二) 内核模块

在操作系统的组成方式上,一直有宏内核与微内核之间的争论,除了这两种方式,还有其他解决办法吗?Linux 给出了答案,那就是”使用模块“。模块允许内核在运行时动态地向其中插入或从中删除代码,无需重新编译整个内核并重新引导系统,可以方便地扩展内核的功能。在驱动开发中,设备驱动程序都是由一个个模块构成的,在我们开始编写驱动前,先看看内核模块是如何使用的吧。

驱动加入到内核的方式

  • 驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序。
    • 添加代码 :在driver目录下添加相应驱动代码
    • Makefile :本级目录添加Makefile,上级目录修改Makefile;将新添加的源码加入编译过程
      1. 本级 Makefile
      2. obj-$(CFG_XXX) += xxx.o
        xxx.objs := xxx-a.o xxx-b.o xxx-c.o
        
        1
        2
        3
        4

        3. 上一级 Makefile
        4. ```makefile
        obj-$(CFG_XXX) += dir_name/
  • 将驱动编译成模块(Linux 下模块扩展名为 .ko),在Linux 内核启动以后使用相应命令加载驱动模块。
    • 内核模块是Linux内核向外部提供的一个插口
    • 内核模块是具有独立功能的程序,他可以被单独编译,但不能单独运行。他在运行时被链接到内核作为内核的一部分在内核空间运行
    • 内核模块便于驱动、文件系统等的二次开发
    • 根据源码路径,交叉编译路径,添加Makefile(和上面相同),编译为.ko文件,命令为: make -C /kernel/source/location/ SUBDIRS=$PWD modules

内核模块组成

模块入口函数

1
module_init(xxx_init);
  • module_init 函数用来向 Linux 内核注册一个模块加载函数,
  • 参数 xxx_init 就是需要注册的具体函数(理解是模块的构造函数)
  • 当加载驱动的时, xxx_init 这个函数就会被调用

模块退出函数

1
module_exit(xxx_exit);
  • module_exit函数用来向 Linux 内核注册一个模块卸载函数,
  • 参数 xxx_exit 就是需要注册的具体函数(理解是模块的析构函数)
  • 当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用

模块许可证明

1
MODULE_LICENSE("GPL") //添加模块 LICENSE 信息 ,LICENSE 采用 GPL 协议

模块参数(可选)

模块参数是一种内核空间与用户空间的交互方式,只不过是用户空间 –> 内核空间单向的,他对应模块内部的全局变量

模块信息(可选)

1
MODULE_AUTHOR("huashan sun <sunhuashan624@gamil.com>") //添加模块作者信息

模块打印 printk

printk在内核中用来记录日志信息的函数,只能在内核源码范围内使用。和printf非常相似。 printk函数主要做两件事情:①将信息记录到log中 ②调用控制台驱动来将信息输出

  • printk 可以根据日志级别对消息进行分类,一共有 8 个日志级别

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #define KERN_SOH  "\001" 
    #define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
    #define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
    #define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
    #define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */
    #define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
    #define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
    #define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
    #define KERN_DEBUG KERN_SOH "7" /* 调试信息 */
  • 以下代码就是设置“gsmi: Log Shutdown Reason\n”这行消息的级别为 KERN_EMERG。

    1
    printk(KERN_DEBUG"gsmi: Log Shutdown Reason\n");

    如果使用 printk 的时候不显式的设置消息级别,那 么printk 将会采用默认级别MESSAGE_LOGLEVEL_DEFAULT,默认为 4

  • 在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:

    1
    #define CONSOLE_LOGLEVEL_DEFAULT 7

    CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。

    这个就是 printk 和 printf 的最大区别,可以通过消息级别来决定哪些消息可以显示在控制台上。默认消息级别为 4,4 的级别比 7 高,所示直接使用 printk 输出的信息是可以显示在控制台上的。

模块相关的命令

加载模块

  • insmod XXX.ko
    • 为模块分配内核内存、将模块代码和数据装入内存、通过内核符号表解析模块中的内核引用、调用模块初始化函数(module_init)
    • insmod要加载的模块有依赖模块,且其依赖的模块尚未加载,那么该insmod操作将失败
  • modprobe XXX.ko
    • 加载模块时会同时加载该模块所依赖的其他模块,提供了模块的依赖性分析、错误检查、错误报告
    • modprobe 提示无法打开“modules.dep”这个文件 ,输入 depmod 命令即可自动生成 modules.dep

卸载模块

  • rmmod XXX.ko

查看模块信息

  • lsmod
    • 查看系统中加载的所有模块及模块间的依赖关系
  • modinfo (模块路径)
    • 查看详细信息,内核模块描述信息,编译系统信息

hello world 模块实战

光说不练假把式,最后,我们以一个 hello world 模块实战结束,正式进入驱动开发的世界。这个系列的实战例程参考了 《Linux Device Driver 3rd》这本书,因为没有涉及到真实硬件,所以选择在 WSL 下进行开发,感兴趣的读者可以参考 WSL2的安装、应用、内核模块编译安装

一个最简单的 hello 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <linux/init.h>
#include <linux/module.h>

static int __init hello_init(void)
{
printk( KERN_DEBUG "hello world init\n");
return 0;
}

static void __exit hello_exit(void)
{
printk( KERN_DEBUG "hello world exit\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

先看头文件,其中 init.h 包括 __init 宏和 __exit 宏,module.h 包含 模块相关的宏,这两个头文件在驱动程序中是必不可少的。

1
2
#include <linux/init.h>
#include <linux/module.h>

紧接着是模块的入口和出口函数,这里我们只是进行简单的打印。

1
2
3
4
5
6
7
8
9
10
static int __init hello_init(void)
{
printk( KERN_DEBUG "hello world init\n");
return 0;
}

static void __exit hello_exit(void)
{
printk( KERN_DEBUG "hello world exit\n");
}

最后是模块相关的宏,其中最重要的是 module_initmodule_exit 宏。当使用 insmodmodprobe 宏安装模块时,使用 module_init 注册的函数会被调用,这里调用的是 hello_init 函数,类似的,当模块被移除时,调用的是使用 module_exit 注册的函数,这里是 hello_exit。MODULE_LICENSE 指定的是模块使用的开源协议。

1
2
3
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

编译模块

当以模块的形式编写驱动程序时,需要在 Makefile 文件中指定内核的源码路径:

1
2
3
4
5
6
7
8
9
KERNEL_PATH := /home/sunhuashan/workplace/wsl-linux-5.10.16.3
PWD := $(shell pwd)

obj-m := hello.o

build_modules:
$(MAKE) -C $(KERNEL_PATH) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNEL_PATH) M=$(PWD) clean

在这个简单的 Makefile 文件中,各个变量的意义如下:

  • KERNEL_PATH :内核源码路径
  • PWD:模块所在路径,这里使用 shell 中的 pwd 命令获取了 Makefile 所在的目录,一般而已,这也是应该是模块源码所在的目录
  • obj-m:模块所依赖的目标文件,应该还源码中的源文件一一对应
  • 剩下的就是简单的 Makefile 语法了,值得注意的是,使用 -C 指定源码路径,M= 指定模块路径, modules 表示编译模块, clean 表示清除模块

实战

  • make 编译
1
2
3
4
5
6
7
8
$ make build_modules 
make -C /home/sunhuashan/workplace/wsl-linux-5.10.16.3 M=/home/sunhuashan/workplace/ldd3/ours/misc_modules modules
make[1]: Entering directory '/home/sunhuashan/workplace/wsl-linux-5.10.16.3'
CC [M] /home/sunhuashan/workplace/ldd3/ours/misc_modules/hello.o
MODPOST /home/sunhuashan/workplace/ldd3/ours/misc_modules/Module.symvers
CC [M] /home/sunhuashan/workplace/ldd3/ours/misc_modules/hello.mod.o
LD [M] /home/sunhuashan/workplace/ldd3/ours/misc_modules/hello.ko
make[1]: Leaving directory '/home/sunhuashan/workplace/wsl-linux-5.10.16.3'
  • insmod 安装
1
$ sudo insmod hello.ko
  • dmesg 查看输出日志
1
2
$ dmesg | grep hello
[52394.010385] hello world init

可以看到,我们的 hello_init 函数被调用,并且打印了一行输出。

  • rmmod 卸载
1
2
3
4
$ sudo rmmod hello
$ dmesg | grep hello
[52394.010385] hello world init
[52601.531399] hello world exit

可以看到,我们的 hello_exit 函数被调用,并且打印了一行输出。