版权声明
本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。
Copyright (C) 2021 凌云物网智科实验室·郭工
Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292
3.2 Linux下hello驱动开发
我们在学习编程语言时,通常都是从 “hello world” 例子开始。作为一个编程入门指导最简单的方法,我们也将以 hello 模块为例,来讲解Linux内核里的驱动模块的开发和使用流程。
3.2.1 编写 hello 驱动模块文档
创建驱动学习的项目文件夹。
guowenxue@ubuntu20:~$ mkdir drivers && cd driver
编写 hello.c
驱动模块源码如下。
guowenxue@ubuntu20:~/drivers$ vim hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static __init int hello_init(void)
{
printk(KERN_ALERT "hello module installed.\n");
return 0;
}
static __exit void hello_exit(void)
{
printk(KERN_ALERT "hello module removed.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("GuoWenxue <guowenxue@gmail.com>");
Linux内核模块并不是用户程序,运行方式上有很多不同之处,编译过程上也是有显著不同,接下来我们将对上面这个简单的内核源码做些解释。
3.2.2 内核版本和头文件
源码开始的三个头文件,这是所有Linux内核驱动模块源码必须要包含的几个头文件。需要注意的时,我们平时所用的头文件大多在 /usr/include
,这个目录是用来存放各种在用户态下运行的库的C/C++头文件。而Linux内核模块编译时需要使用Linux内核的源代码(主要是头文件),运行时也是在内核态中,不允许使用各种用户态库,这样就得知道 Linux内核头文件所在位置。
在这里我们将在 ubuntu-20.04 X86 服务器上做测试,因为 ubuntu 系统可能会经常升级 Linux内核,这样不同的内核源码肯定不一样。而我们的Linux内核模块将在当前系统下编译并运行,那我们就得找到当前系统所用的 Linux内核头文件。这里我们可以使用 uname
命令查看当前的内核版本,如下执行结果显示当前系统正使用 5.15.0-88
这个内核版本。
guowenxue@ubuntu20:~$ uname -a
Linux ubuntu20 5.15.0-88-generic #98~20.04.1-Ubuntu SMP Mon Oct 9 16:43:45 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
guowenxue@ubuntu20:~$ uname -r
5.15.0-88-generic
在 /lib/modules
路径下,存放了当前系统安装的所有内核版本,我们也可以使用 dpkg
命令来找出所安装的 Linux内核。
guowenxue@ubuntu20:~$ ls /lib/modules
5.11.0-27-generic 5.11.0-37-generic 5.11.0-38-generic 5.11.0-40-generic 5.15.0-41-generic 5.15.0-43-generic 5.15.0-75-generic 5.15.0-79-generic 5.15.0-86-generic 5.15.0-87-generic 5.15.0-88-generic
guowenxue@ubuntu20:~$ sudo dpkg --list | grep linux-image
rc linux-image-5.11.0-27-generic 5.11.0-27.29~20.04.1 amd64 Signed kernel image generic
rc linux-image-5.11.0-37-generic 5.11.0-37.41~20.04.2 amd64 Signed kernel image generic
rc linux-image-5.11.0-38-generic 5.11.0-38.42~20.04.1 amd64 Signed kernel image generic
rc linux-image-5.11.0-40-generic 5.11.0-40.44~20.04.2 amd64 Signed kernel image generic
rc linux-image-5.15.0-41-generic 5.15.0-41.44~20.04.1 amd64 Signed kernel image generic
rc linux-image-5.15.0-43-generic 5.15.0-43.46~20.04.1 amd64 Signed kernel image generic
rc linux-image-5.15.0-75-generic 5.15.0-75.82~20.04.1 amd64 Signed kernel image generic
ii linux-image-5.15.0-79-generic 5.15.0-79.86~20.04.2 amd64 Signed kernel image generic
rc linux-image-5.15.0-86-generic 5.15.0-86.96~20.04.1 amd64 Signed kernel image generic
rc linux-image-5.15.0-87-generic 5.15.0-87.97~20.04.1 amd64 Signed kernel image generic
ii linux-image-5.15.0-88-generic 5.15.0-88.98~20.04.1 amd64 Signed kernel image generic
ii linux-image-generic-hwe-20.04 5.15.0.88.98~20.04.46 amd64 Generic Linux kernel image
我们也可以使用 apt purge
命令移除无用的 Linux 内核。
guowenxue@ubuntu20:~$ sudo apt purge -y linux-image-5.11.0* linux-image-5.15.0-4*
guowenxue@ubuntu20:~$ ls /lib/modules
5.15.0-79-generic 5.15.0-86-generic 5.15.0-87-generic 5.15.0-88-generic
事实上Linux内核源码在编译时,需要进入到一个已经编译过的Linux内核源码路径下去编译,并在这个路径下找相应的头文件。而这个路径通常为 /lib/modules/内核版本/build,而该文件是指向 /usr/src/linux-headers-5.15.0-88-generic
的符号链接。
guowenxue@ubuntu20:~$ ls /lib/modules/5.15.0-88-generic/
build kernel modules.alias.bin modules.builtin.alias.bin modules.builtin.modinfo modules.dep.bin modules.order modules.symbols vdso
initrd modules.alias modules.builtin modules.builtin.bin modules.dep modules.devname modules.softdep modules.symbols.bin
guowenxue@ubuntu20:~$ ls -l /lib/modules/5.15.0-88-generic/build
lrwxrwxrwx 1 root root 40 Oct 9 23:46 /lib/modules/5.15.0-88-generic/build -> /usr/src/linux-headers-5.15.0-88-generic
接下来我们再看 /usr/src/linux-headers-5.15.0-88-generic
里的文件,它里面的文件大部分也都是符号链接。
guowenxue@ubuntu20:~$ ls -l /usr/src/linux-headers-5.15.0-88-generic
total 1804
drwxr-xr-x 3 root root 4096 Nov 1 06:50 arch
lrwxrwxrwx 1 root root 41 Oct 9 23:46 block -> ../linux-hwe-5.15-headers-5.15.0-88/block
lrwxrwxrwx 1 root root 41 Oct 9 23:46 certs -> ../linux-hwe-5.15-headers-5.15.0-88/certs
lrwxrwxrwx 1 root root 42 Oct 9 23:46 crypto -> ../linux-hwe-5.15-headers-5.15.0-88/crypto
lrwxrwxrwx 1 root root 49 Oct 9 23:46 Documentation -> ../linux-hwe-5.15-headers-5.15.0-88/Documentation
lrwxrwxrwx 1 root root 43 Oct 9 23:46 drivers -> ../linux-hwe-5.15-headers-5.15.0-88/drivers
lrwxrwxrwx 1 root root 38 Oct 9 23:46 fs -> ../linux-hwe-5.15-headers-5.15.0-88/fs
drwxr-xr-x 4 root root 4096 Nov 1 06:50 include
lrwxrwxrwx 1 root root 40 Oct 9 23:46 init -> ../linux-hwe-5.15-headers-5.15.0-88/init
lrwxrwxrwx 1 root root 44 Oct 9 23:46 io_uring -> ../linux-hwe-5.15-headers-5.15.0-88/io_uring
lrwxrwxrwx 1 root root 39 Oct 9 23:46 ipc -> ../linux-hwe-5.15-headers-5.15.0-88/ipc
lrwxrwxrwx 1 root root 42 Oct 9 23:46 Kbuild -> ../linux-hwe-5.15-headers-5.15.0-88/Kbuild
lrwxrwxrwx 1 root root 43 Oct 9 23:46 Kconfig -> ../linux-hwe-5.15-headers-5.15.0-88/Kconfig
drwxr-xr-x 2 root root 4096 Nov 1 06:50 kernel
lrwxrwxrwx 1 root root 39 Oct 9 23:46 lib -> ../linux-hwe-5.15-headers-5.15.0-88/lib
lrwxrwxrwx 1 root root 44 Oct 9 23:46 Makefile -> ../linux-hwe-5.15-headers-5.15.0-88/Makefile
lrwxrwxrwx 1 root root 38 Oct 9 23:46 mm -> ../linux-hwe-5.15-headers-5.15.0-88/mm
-rw-r--r-- 1 root root 1815928 Oct 9 23:46 Module.symvers
lrwxrwxrwx 1 root root 39 Oct 9 23:46 net -> ../linux-hwe-5.15-headers-5.15.0-88/net
lrwxrwxrwx 1 root root 43 Oct 9 23:46 samples -> ../linux-hwe-5.15-headers-5.15.0-88/samples
drwxr-xr-x 7 root root 12288 Nov 1 06:50 scripts
lrwxrwxrwx 1 root root 44 Oct 9 23:46 security -> ../linux-hwe-5.15-headers-5.15.0-88/security
lrwxrwxrwx 1 root root 41 Oct 9 23:46 sound -> ../linux-hwe-5.15-headers-5.15.0-88/sound
drwxr-xr-x 4 root root 4096 Nov 1 06:50 tools
lrwxrwxrwx 1 root root 42 Oct 9 23:46 ubuntu -> ../linux-hwe-5.15-headers-5.15.0-88/ubuntu
lrwxrwxrwx 1 root root 39 Oct 9 23:46 usr -> ../linux-hwe-5.15-headers-5.15.0-88/usr
lrwxrwxrwx 1 root root 40 Oct 9 23:46 virt -> ../linux-hwe-5.15-headers-5.15.0-88/virt
接下来我们看 /usr/src/ 可知,linux-headers-5.15.0-88-generic 存放编译的内核模块,而 linux-hwe-5.15-headers-5.15.0-88 则存放内核头文件。
guowenxue@ubuntu20:~$ ls /usr/src/
linux-headers-5.15.0-79-generic linux-headers-5.15.0-88-generic linux-hwe-5.15-headers-5.15.0-79 linux-hwe-5.15-headers-5.15.0-88
3.2.1.2 驱动模块的加载和卸载
Linux驱动有两种运行方式,第一种就是将驱动编译进Linux内核中,这样当Linux内核启动时就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用”insmod”命令加载驱动模块。
在调试的时候一般编译成模块,这样不需要编译整个Linux代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。最大的好处就是方便。
模块加载和卸载注册函数如下:
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内核映像文件的相应段中,这样hello_init将会被链接进.init.text段中,而hello_exit将会被链接进.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函数就会被调用。
3.2.1.3 编写hello模块C文件
#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("GuoWenxue <guowenxue@gmail.com>");
定义了个名为 xxx_init 的驱动入口函数,并且使用了“__init”来修饰。
定义了个名为 xxx_exit 的驱动出口函数,并且使用了“__exit”来修饰。
调用函数 module_init 来声明 xxx_init 为驱动入口函数,当加载驱动的时候 xxx_init函数就会被调用。
调用函数module_exit来声明xxx_exit为驱动出口函数,当卸载驱动的时候xxx_exit函数就会被调用。
添加LICENSE和作者信息,是来告诉内核,该模块带有一个自由许可证;没有这样的说明,在加载模块的时内核会“抱怨”。
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
3.2.1.3 关于printk的说明
在 Linux 内核中没有 printf 这个函数。printk 相当于 printf 的孪生兄妹,printf运行在用户态,printk 运行在内核态。模块能够调用printk正式因为在insmod加载了它之后,模块被链接到内核并且可存取内核的公用符号。字符串KERN_ALERT是优先级。
printk支持分级打印调试,只有优先级高于 7 的消息才能显示在控制台上。这个就是 printk 和 printf 的最大区别。
这 8 个消息级别定义在文件 include/linux/kern_levels.h 里面,定义如下:
#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命令清除之前的打印信息。
3.2.2运行测试
3.2.2.1在X86主机上测试内核模块
创建x876文件夹,在里面编译并测试hello内核模块。
guowenxue@ubuntu:~$ mkdir x86
guowenxue@ubuntu:~$ cd x86/
guowenxue@ubuntu:~/x86$ ls
kernel_hello.c Makefile
guowenxue@ubuntu:~/x86$ vim Makefile
KERNAL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
obj-m := kernel_hello.o
modules:
$(MAKE) -C $(KERNAL_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
guowenxue@ubuntu:~/x86$ make
make -C /lib/modules/5.4.0-124-generic/build M=/home/guowenxue/x86 modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.0-124-generic'
CC [M] /home/guowenxue/x86/kernel_hello.o
Building modules, stage 2.
MODPOST 1 modules
CC [M] /home/guowenxue/x86/kernel_hello.mod.o
LD [M] /home/guowenxue/x86/kernel_hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.0-124-generic'
make[1]: Entering directory '/home/guowenxue/x86'
make[1]: Leaving directory '/home/guowenxue/x86'
guowenxue@ubuntu:~/x86$ ls
kernel_hello.c kernel_hello.ko Makefile
用dmesg查看Linux内核打印信息,dmesg -c将会清除之前Linux 内核的打印信息
安装Linux模块,并查看内核打印信息:
guowenxue@ubuntu:~/x86$ sudo insmod kernel_hello.ko
guowenxue@ubuntu:~/x86$ sudo rmmod kernel_hello
guowenxue@ubuntu:~/x86$ sudo insmod kernel_hello.ko
guowenxue@ubuntu:~/x86$ dmesg | tail -3
[1120961.091209] hello world
[1121083.590510] Goodbye
[1121093.936166] hello world
用lsmod命令查看当前linux内核安装了的内核模块
guowenxue@ubuntu:~/x86$ lsmod
Module Size Used by
kernel_hello 16384 0
btrfs 1241088 0
xor 24576 1 btrfs
3.2.2.2在ARM板上测试内核模块
guowenxue@ubuntu:~/alientek/driver/01hello$ ls
kernel_hello.c kernel_hello.ko Makefile
guowenxue@ubuntu:~/alientek/driver/01hello$ vim Makefile
KERNAL_DIR := /home/guowenxue/imx6ull/bsp/kernel/linux-imx
PWD :=$(shell pwd)
obj-m := kernel_hello.o
modules:
$(MAKE) -C $(KERNAL_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
~
guowenxue@ubuntu:~/driver/01hello$ make
make -C /home/zying/imx6ull/bsp/kernel/linux-imx M=/home/guowenxue/driver/01hello modules
make[1]: Entering directory '/home/guowenxue/imx6ull/bsp/kernel/linux-imx'
CC [M] /home/guowenxue/driver/01hello/kernel_hello.o
MODPOST /home/guowenxue/driver/01hello/Module.symvers
CC [M] /home/guowenxue/driver/01hello/kernel_hello.mod.o
LD [M] /home/guowenxue/driver/01hello/kernel_hello.ko
make[1]: Leaving directory '/home/guowenxue/imx6ull/bsp/kernel/linux-imx'
make[1]: Entering directory '/home/guowenxue/driver/01hello'
make[1]: Leaving directory '/home/guowenxue/driver/01hello'
把生成的kernel_hello.ko 拷贝到tftp目录下:
cp kernel_hello.ko /tftp/guowenxue/ -f
3.2.2.3关于Makefile文件的说明
KERNAL_DIR是指定开发板所运行的源码路径,并且这个linux内核源码必须make menuconfig并且make过的,因为Linux内核的一个模块可能依赖于另一个模块,如果另一个没有编译则出问题。所以Linux内核必须编译过,这样才能确认这种依赖关系;
obj-m +:= kernel_hello.o 该行告诉Makefile要将kernel_hello.c源码编译生成内核模块kernel_hello.ko;
$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules ,-C:把工作目录切换到-C后面指定的参数目录,M是Makefile里面的一个变量,作用是回到当前目录继续读取Makefile。当前使用make命令编译内核驱动模块时,将会进入到KERNAL_DIR指定的linux内核源码中去编译,并在当前目录下生成很多临时文件以及驱动模块文件kernel_hello.ko;
clear目标将编译linux内核过程生成的一些临时文件全部删掉;
3.2.2.4开发板上测试:
先把生成的.ko文件传输给开发板,这里使用wget命令从服务器下载到开发板。
root@igkboard:~# wget http://studio.iot-yun.club:7000/pub/zouying/kernel_hello.ko
Connecting to studio.iot-yun.club:7000 (27.16.213.10:7000)
saving to 'kernel_hello.ko'
kernel_hello.ko 100% |**********************************************************************************| 4028 0:00:00 ETA
'kernel_hello.ko' saved
然后再开发板上安装驱动
root@igkboard:~/01hello# insmod kernel_hello.ko
root@igkboard:~# dmesg | tail -1
[ 1401.769468] hello world
root@igkboard:~/01hello# lsmod
Module Size Used by
kernel_hello 16384 0
rtl8188fu 991232 0
imx_rngc 16384 0
rng_core 20480 1 imx_rngc
secvio 16384 0
error 20480 1 secvio
root@igkboard:~/01hello# rmmod kernel_hello
root@igkboard:~/01hello# dmesg | tail -1
[ 1652.733679] Goodbye
3.2.3加载卸载驱动模块
驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块:insmod和 modprobe
insmod 命令不能解决模块的依赖关系
modprobe 会分析模块的依赖关系
驱动模块的卸载使用命令“rmmod”即可。
也可以使用“modprobe -r”命令卸载驱动。
rmmod chrdevbase.ko
卸载以后使用 lsmod 命令查看 chrdevbase 这个模块还存不存在。