Linux bsp驱动学习二:驱动模块与Hello World

​ 接下来将编写第一个驱动模块,该驱动模块功能是在加载时输出“Hello World”,卸载模块时输出“Goodbye World”。首先先对模块的重要组成部分进行介绍

一、驱动模块的组成

​ 一个驱动模块的主要组成部分由如下组成,如下表所示,表中表示的是一个**规范的驱动模块应该包含的结构,同样也是一个规范的在源文件的编写顺序**,不按以下的顺序编写也不会报错,只是依靠以下顺序比较规范。

驱动模块的组成部分
头文件(必选)
模块参数(可选)
模块功能函数(可选)
其他(可选)
模块加载函数(必选)
模块卸载函数(必选)
模块许可声明(必选)

1、头文件(必选)

​ 驱动模块会使用内核中的许多函数,所以需要包含必要的头文件。有两个头文件是所有驱动模块都必须包含的,分别是:

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

​ module.h文件包含了加载模块时需要使用的大量符号和函数定义。init.h包含了模块加载函数和模块释放函数的宏定义。

2、模块参数(可选)

​ 模块参数是驱动模块加载时,需要传递给驱动模块的参数。如果一个驱动模块需要完成两种功能,那么就可以通过模块参数选择使用哪一种功能。这样在模块内部,就可以控制硬件完成不同的功能。

3、模块加载函数(必选)

​ 模块加载函数是模块加载时,需要执行的函数,这是模块的初始化函数,就如 main()
函数一样。

4、模块卸载函数(必须)

​ 模块卸载函数是模块卸载时,需要执行的函数,这里清除了加载函数里分配的资源。

5、模块许可声明(必须)

​ 模块许可申明表示模块受内核支持的程度。有许可权的模块会更受到开发人员的重 视。需要使用 MODULE_LICENSE 表示该模块的许可权限。内核可以识别的许可权限如下:

1
2
3
4
5
6
MODULE_LICENSE("GPL"); 							/*任一版本的 GNU 公共许可权*/
MODULE_LICENSE("GPL v2"); /*GPL 版本 2 许可权*/
MODULE_LICENSE("GPL and additional rights"); /*GPL 及其附加许可权*/
MODULE_LICENSE("Dual BSD/GPL"); /*BSD/GPL 双重许可权*/
MODULE_LICENSE("Dual MPL/GPL"); /*MPL/GPL 双重许可权*/
MODULE_LICENSE("Proprietary"); /*专有许可权*/

​ 如果一个模块没有包含任何许可权,那么就会认为是不符合规范的。这时,内核加载 这种模块时,会收到内核加载了一个非标准模块的警告。开发人员不喜欢维护这种没有遵 循许可权标准的内核模块。

​ 以 GPL 为例,说明许可权的意义。GPL 是 General Public License 的缩写,表示通用公共许可证。GNU 通用公共许可证可以保证你有发布自由软件的自由;保证你能收到源程序 或者在你需要时能得到它;保证你能修改软件或将它的一部分用于新的自由软件。

​ 许可权决定了模块在被他人使用或者商用时,是否需要支付授权费用。

二、Hello World模块

​ 以hello模块为例讲解LInux内核模块的编写、编译以及使用过程。

1、创建代码存放路径

1
2
mkdir -p world/01_hello_kernel
cd world/01_hello_kernel

2、编写hello模块C文件

1
vim hello_kernel.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*********************************************************************************
* Copyright: (C) 2023 Noah<njy_roxy@outlook.com>
* All rights reserved.
*
* Filename: hello.c
* Description: This file
*
* Version: 1.0.0(2023年04月15日)
* Author: Noah <njy_roxy@outlook.com>
* ChangeLog: 1, Release initial version on "2023年04月15日 06时08分24秒"
*
********************************************************************************/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

static __init int hello_init(void)
{
printk(KERN_ALERT "hello world\n");

return 0;
}

static __exit void hello_exit(void)
{
printk(KERN_ALERT "Goodbye\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("NongJieYing <njy_roxy@outlook.com>");

定义了个名为 xxx_init 的驱动入口函数,并且使用了“__init”来修饰。

定义了个名为 xxx_exit 的驱动出口函数,并且使用了“__exit”来修饰。

调用函数 module_init 来声明 xxx_init 为驱动入口函数,当加载驱动的时候 xxx_init函数就会被调用。

调用函数module_exit来声明xxx_exit为驱动出口函数,当卸载驱动的时候xxx_exit函数就会被调用。

添加LICENSE和作者信息,是来告诉内核,该模块带有一个自由许可证;没有这样的说明,在加载模块的时内核会“抱怨”。

1
2
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息

3、驱动模块的加载和卸载

​ Linux驱动有两种运行方式,第一种就是将驱动编译进Linux内核中,这样当Linux内核启动时就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用”insmod”命令加载驱动模块。

在调试的时候一般编译成模块,这样不需要编译整个Linux代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。最大的好处就是方便。

模块加载和卸载注册函数如下:

1
2
3
module_init(xxx_init);	//注册模块加载函数

module_exit(xxx_exit); //注册模块卸载函数

__init、 __exit这两个宏是定义在include/linux/init.h中:

static __init int hello _init(void) 宏就被展开为 static __section(.init.text) __cold notrace int hello_init(void)

static __exit int hello _init(void) 宏就被展开为 static __section(.exit.text) __exitused__cold notrace int hello_exit

__section为gcc链接选项,他表示把该函数链接到Linux内核映像文件的相应段中,__init中的__section(.init.text),即将指针变量链接到**.init.text段中,同理__exit 中的__section(.exit.text)将指针变量链接到.exit.text**段中。

​ 被链接进这两段中的函数的代码在调试完之后,内核将会自动释放他们所占用的内存资源。因为这些函数只需要初始化或退出一次,所以hello_init()和hello_exit()函数做好在前面加上__init__exit

module_inti(hello_init)宏被展开为:

satic int (*initcall_t)(void) __initcall_hello_init6_used_attribute_((_section_("initcall""6"".init")))=hello_init

​ 这段代码也就是定义了一个叫 __initcall_hello_init6的函数指针,他指向hello_init这个函数,gcc的链接选项__attribute____section__将该指针变量链接到linux内核映像的.initcall段中。linux系统在启动时,完成CPU和板级初始化之后,就会从该段中读入所有的模块初始化函数执行。每一个Linux内核模块都需要使用module_init()和module_exit()宏来修饰,这样系统启动时才能自动调用并初始化他们。

当使用”insmod”命令来加载驱动的时候,xxx_init函数就会被调用。

当使用”rmmod”命令卸载具体驱动的时候,xxx_exit函数就会被调用。

4、关于printk的说明

​ 在 Linux 内核中没有 printf 这个函数。printk 相当于 printf 的孪生兄妹,printf运行在用户态,printk 运行在内核态。模块能够调用printk正式因为在insmod加载了它之后,模块被链接到内核并且可存取内核的公用符号。字符串KERN_ALERT是优先级。

printk支持分级打印调试,只有优先级高于 7 的消息才能显示在控制台上。这个就是 printk 和 printf 的最大区别。

这 8 个消息级别定义在文件 include/linux/kern_levels.h 里面,定义如下:

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" /* 调试信息 */

一共定义了 8 个级别,其中 0 的优先级最高,7 的优先级最低。

Linux内核中printk()语句是否打印到串口终端上,与u-boot里的bootargs参数中的loglelev = 7相关,只有低于loglevel级别的信息才会打印到控制终端上,否则不会在控制中断上输出。这时我们只能通过dmesg命令查看。Linux下的dmesg命令的可以查看linux内核所有的打印信息,他们记录在**/var/log/messages系统日志文件中。linux内核的打印信息很多,我们可以使用dmesg -c**命令清除之前的打印信息。

  • 注意的一点是,内核里面并不能完美的支持浮点数操作。在内核中使用浮点数的时候,除了要人工保存和恢复浮点寄存器外,还有一些琐碎的事情要做。为了避免麻烦,通常不在内核中使用浮点数。除此之外,printk()函数也不支持浮点类型

三、编译Hello World模块

​ 在对 Hello World 模块进行编译时,需要满足一定的条件。

1、编译内核模块的条件

​ 正确的编译内核模块,应该满足以下重要的先决条件:

  1. 确保使用正确版本的编译工具、模块工具和其他必要的工具。不同版本的内核需要不同版本的编译工具。
  2. 应该有一份内核源码,该源码的版本应该和系统目前使用的内核版本一致。这是因为模块的编译,需要借助内核源码中的一些函数或者工具。
  3. 内核源码应该至少编译过一次,也就是执行过 make 命令。

2、创建Makefile文件

1
vim Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
KERNEL_DIR := /home/noah/imx6ull/bsp/kernel/linux-imx 
PWD :=$(shell pwd)
appname += kernel_hello
obj-m := $(appname).o

modules:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
@make clear
clear:
@rm -f *.o *.cmd *.mod *.mod.c
@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
@rm -f .*ko.cmd .*.o.cmd .*.o.d
@rm -f *.unsigned

clean:
@rm -f *.ko
  • 第1~2行定义了 KERNEL_DIR和 PWD 变量。KERNEL_DIR是内核路径变量, PWD 是由执行 pwd 命令得到的当前模块路径。
  • modules以冒号结尾,表示 Makefile 文件的一个功能选项
  • make 的语法是“Make –C 内核路径 M=模块路径 modules”。该语句会执行内核模块的编译
  • $(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules ,-C:把工作目录切换到-C后面指定的参数目录,M是Makefile里面的一个变量,作用是回到当前目录继续读取Makefile。当前使用make命令编译内核驱动模块时,将会进入到KERNAL_DIR指定的linux内核源码中去编译,并在当前目录下生成很多临时文件以及驱动模块文件kernel_hello.ko;
  • clear是删除编译过程的中间文件的命令
  • obj-m意思是将 hello.o 编译成 hello.ko 模块。如果要编译其他模块时,只要将 hello.o 中的 hello 改为模块的文件名
  • obj-m +:= kernel_hello.o 该行告诉Makefile要将kernel_hello.c源码编译生成内核模块kernel_hello.ko

四、实际测试

1、模块的操作

​ Linux 为用户提供了 modutils 工具,用来操作模块。这个工具集主要包括:

  • insmod 命令加载模块。使用 insmod hello.ko 可以加载 hello.ko 模块。如果模块带有参数,那么使用下面的格式可以传递参数给模块: insmod 模块.ko 参数 1=值 1 参数 2=值 2 参数 3=值 3 /参数之间没有逗号/
  • rmmod 命令卸载模块。如果模块没有被使用,那么执行 rmmod hello.ko 就可以卸载 hello.ko 模块。
  • modprobe 命令是比较高级的加载和删除模块命令,其可以解决模块之间的依赖性问题。
  • lsmod 命令列出已经加载的模块和信息。在 insmod之前后分别执行该命令, 可以知道模块是否被加载。
  • modinfo 命令用于查询模块的相关信息,比如作者、版权等。

2、加载模块后文件系统变化

​ 当使用 insmod kernel_hello.ko 加载模块后,文件系统发生变化

  • /proc/modules 中添加kernel_hello模块的信息。(lsmod 命令就是通过读取/proc/modules 文件列出内核当前已经加载的模块信息的。)
  • /proc/devices 文件没有变化,因为 kernel_hello.ko 模块并不是一个设备模块,当模块是一个设备的驱动时,在模块中,需要新建一个设备文件。
  • 在/sys/module/目录会增加 kernel_hello这个模块的基本信息。
  • 在/sys/module/目录下会增加一个 kernel_hello目录。

3、测试操作

驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块:insmod和 modprobe

  • insmod 命令不能解决模块的依赖关系
  • modprobe 会分析模块的依赖关系

驱动模块的卸载使用命令“rmmod”即可。

也可以使用“modprobe -r”命令卸载驱动。

rmmod chrdevbase.ko
卸载以后使用 lsmod 命令查看 chrdevbase 这个模块还存不存在。

1
2
3
make
# 把生成的kernel_hello.ko 拷贝到tftp目录下
cp kernel_hello.ko ~/tftp -f
1
2
# 开发板下载
tftp -gr kernel_hello.ko 192.168.0.202
1
2
3
4
5
6
7
# 开发板上测试,也可在安装后使用lsmod查看已安装的模块
root@igkboard:~/workdir/01# insmod kernel_hello.ko
root@igkboard:~/workdir/01# dmesg | tail -1
[66695.584784] hello world
root@igkboard:~/workdir/01# rmmod kernel_hello.ko
root@igkboard:~/workdir/01# dmesg | tail -1
[66718.568805] Goodbye