Linux bsp驱动学习二:驱动模块与Hello World
Linux bsp驱动学习二:驱动模块与Hello World
接下来将编写第一个驱动模块,该驱动模块功能是在加载时输出“Hello World”,卸载模块时输出“Goodbye World”。首先先对模块的重要组成部分进行介绍
一、驱动模块的组成
一个驱动模块的主要组成部分由如下组成,如下表所示,表中表示的是一个**规范的驱动模块应该包含的结构
,同样也是一个规范的在源文件的编写顺序
**,不按以下的顺序编写也不会报错,只是依靠以下顺序比较规范。
驱动模块的组成部分 |
---|
头文件(必选) |
模块参数(可选) |
模块功能函数(可选) |
其他(可选) |
模块加载函数(必选) |
模块卸载函数(必选) |
模块许可声明(必选) |
1、头文件(必选)
驱动模块会使用内核中的许多函数,所以需要包含必要的头文件。有两个头文件是所有驱动模块都必须包含的,分别是:
1 |
module.h文件包含了加载模块时需要使用的大量符号和函数定义。init.h包含了模块加载函数和模块释放函数的宏定义。
2、模块参数(可选)
模块参数是驱动模块加载时,需要传递给驱动模块的参数。如果一个驱动模块需要完成两种功能,那么就可以通过模块参数选择使用哪一种功能。这样在模块内部,就可以控制硬件完成不同的功能。
3、模块加载函数(必选)
模块加载函数是模块加载时,需要执行的函数,这是模块的初始化函数,就如 main()
函数一样。
4、模块卸载函数(必须)
模块卸载函数是模块卸载时,需要执行的函数,这里清除了加载函数里分配的资源。
5、模块许可声明(必须)
模块许可申明表示模块受内核支持的程度。有许可权的模块会更受到开发人员的重 视。需要使用 MODULE_LICENSE 表示该模块的许可权限。内核可以识别的许可权限如下:
1 | MODULE_LICENSE("GPL"); /*任一版本的 GNU 公共许可权*/ |
如果一个模块没有包含任何许可权,那么就会认为是不符合规范的。这时,内核加载 这种模块时,会收到内核加载了一个非标准模块的警告。开发人员不喜欢维护这种没有遵 循许可权标准的内核模块。
以 GPL 为例,说明许可权的意义。GPL 是 General Public License 的缩写,表示通用公共许可证。GNU 通用公共许可证可以保证你有发布自由软件的自由;保证你能收到源程序 或者在你需要时能得到它;保证你能修改软件或将它的一部分用于新的自由软件。
许可权决定了模块在被他人使用或者商用时,是否需要支付授权费用。
二、Hello World模块
以hello模块为例讲解LInux内核模块的编写、编译以及使用过程。
1、创建代码存放路径
1 | mkdir -p world/01_hello_kernel |
2、编写hello模块C文件
1 | vim hello_kernel.c |
1 | /********************************************************************************* |
定义了个名为 xxx_init 的驱动入口函数,并且使用了“__init”来修饰。
定义了个名为 xxx_exit 的驱动出口函数,并且使用了“__exit”来修饰。
调用函数 module_init 来声明 xxx_init 为驱动入口函数,当加载驱动的时候 xxx_init函数就会被调用。
调用函数module_exit来声明xxx_exit为驱动出口函数,当卸载驱动的时候xxx_exit函数就会被调用。
添加LICENSE和作者信息,是来告诉内核,该模块带有一个自由许可证;没有这样的说明,在加载模块的时内核会“抱怨”。
1 | MODULE_LICENSE() //添加模块 LICENSE 信息 |
3、驱动模块的加载和卸载
Linux驱动有两种运行方式,第一种就是将驱动编译进Linux内核中,这样当Linux内核启动时就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用”insmod”命令加载驱动模块。
在调试的时候一般编译成模块,这样不需要编译整个Linux代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。最大的好处就是方便。
模块加载和卸载注册函数如下:
1 | module_init(xxx_init); //注册模块加载函数 |
__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 |
一共定义了 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、编译内核模块的条件
正确的编译内核模块,应该满足以下重要的先决条件:
- 确保使用正确版本的编译工具、模块工具和其他必要的工具。不同版本的内核需要不同版本的编译工具。
- 应该有一份内核源码,该源码的版本应该和系统目前使用的内核版本一致。这是因为模块的编译,需要借助内核源码中的一些函数或者工具。
- 内核源码应该至少编译过一次,也就是执行过 make 命令。
2、创建Makefile文件
1 | vim Makefile |
1 | KERNEL_DIR := /home/noah/imx6ull/bsp/kernel/linux-imx |
- 第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 | make |
1 | # 开发板下载 |
1 | 开发板上测试,也可在安装后使用lsmod查看已安装的模块 |