版权声明

本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。

  • Copyright (C) 2021 凌云物网智科实验室·郭工

  • Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292

wechat_pub

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文件的说明

  1. KERNAL_DIR是指定开发板所运行的源码路径,并且这个linux内核源码必须make menuconfig并且make过的,因为Linux内核的一个模块可能依赖于另一个模块,如果另一个没有编译则出问题。所以Linux内核必须编译过,这样才能确认这种依赖关系;

  2. obj-m +:= kernel_hello.o 该行告诉Makefile要将kernel_hello.c源码编译生成内核模块kernel_hello.ko;

  3. $(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules ,-C:把工作目录切换到-C后面指定的参数目录,M是Makefile里面的一个变量,作用是回到当前目录继续读取Makefile。当前使用make命令编译内核驱动模块时,将会进入到KERNAL_DIR指定的linux内核源码中去编译,并在当前目录下生成很多临时文件以及驱动模块文件kernel_hello.ko;

  4. 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 这个模块还存不存在。