3. Linux内核模块实验

3.1. helloworld实验

3.1.1. 实验说明

3.1.1.1. 硬件介绍

本节实验使用Lubancat_RK系列板卡加载和卸载内核模块进行测试。

3.1.2. 实验代码讲解

本章的示例代码目录为: linux_driver/01_helloworld

从前面我们已经知道了内核模块的工作原理,这一小节就开始写代码了,下面就展示一个最简单helloworld框架。

helloworld框架(位于linux_driver/01_helloworld/helloworld.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>

static int __init helloworld_init(void)
{
   printk( "Hello World Module Init\n");
return 0;
}

static void __exit helloworld_exit(void)
{
   printk("Hello World Module Exit\n");
}

module_init(helloworld_init);
module_exit(helloworld_exit);

MODULE_AUTHOR("embedfire <embedfire@embedfire.com>");
MODULE_DESCRIPTION("hello world module");
MODULE_LICENSE("GPL");

接来下理解每一行代码的含义,并最终在Linux上运行这个驱动,验证我们前面的理论,也为下一章驱动打下基础。

3.1.2.1. 代码框架分析

Linux内核模块的代码框架通常由下面几个部分组成:

  • 模块加载函数(必须): 当通过insmod或modprobe命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。

  • 模块卸载函数(必须): 当执行rmmod命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。

  • 模块许可证声明(必须): 许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。

  • 模块参数: 模块参数是模块被加载时,可以传值给模块中的参数。

  • 模块导出符号: 模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。

  • 模块的其他相关信息: 可以声明模块作者等信息。

上面示例的Hello World Module程序只包含上面三个必要部分以及模块的其他信息声明,而模块参数和导出符号将在下一节实验出现。

头文件包含了<linux/init.h>和<linux/module.h>,这两个头文件是写内核模块必须要包含的。 模块初始化函数hello_init调用了printk函数,在内核模块运行的过程中,它不能依赖于C库函数, 因此用不了printf函数,需要使用单独的打印输出函数printk。

完成模块初始化函数之后,还需要调用宏module_init来告诉内核,使用hello_init函数来进行初始化。 模块卸载函数也用printk函数打印字符串,并用宏module_exit在内核注册该模块的卸载函数。 最后,必须声明该模块使用遵循的许可证,这里我们设置为GPL协议。

3.1.2.2. 头文件

前面我们已经接触过了Linux的应用编程,了解到Linux的头文件都存放在/usr/include中。 编写内核模块所需要的头文件,并不在上述说到的目录,而是在Linux内核源码中的include文件夹。

  • #include <linux/module.h>: 包含内核模块信息声明的相关函数。

  • #include <linux/init.h>: 包含了 module_init()和 module_exit()函数的声明。

  • #include <linux/kernel.h>: 包含内核提供的各种函数,如printk。

编写内核模块中经常要使用到的头文件有以下两个:<linux/init.h>和<linux/module.h>。 我们可以看到在头文件前面也带有一个文件夹的名字linux,对应了include下的linux文件夹,我们到该文件夹下,查看这两个头文件都有什么内容。

init.h头文件(位于内核源码 /include/linux/init.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define pure_initcall(fn)            __define_initcall(fn, 0)

#define core_initcall(fn)            __define_initcall(fn, 1)
#define core_initcall_sync(fn)               __define_initcall(fn, 1s)
#define postcore_initcall(fn)                __define_initcall(fn, 2)
#define postcore_initcall_sync(fn)   __define_initcall(fn, 2s)
#define arch_initcall(fn)            __define_initcall(fn, 3)
#define arch_initcall_sync(fn)               __define_initcall(fn, 3s)
#define subsys_initcall(fn)          __define_initcall(fn, 4)
#define subsys_initcall_sync(fn)     __define_initcall(fn, 4s)
#define fs_initcall(fn)                      __define_initcall(fn, 5)
#define fs_initcall_sync(fn)         __define_initcall(fn, 5s)
#define rootfs_initcall(fn)          __define_initcall(fn, rootfs)
#define device_initcall(fn)          __define_initcall(fn, 6)
#define device_initcall_sync(fn)     __define_initcall(fn, 6s)
#define late_initcall(fn)            __define_initcall(fn, 7)
#define late_initcall_sync(fn)               __define_initcall(fn, 7s)

#define __initcall(fn) device_initcall(fn)

#define __exitcall(fn)                                               \
   static exitcall_t __exitcall_##fn __exit_call = fn

Init.h头文件主要包含了一些宏定义,还有内核的initcall机制。

module.h头文件(位于内核源码/include/linux/module.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
   /* Generic info of form tag = "info" */
   #define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)

   #define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)
   #define MODULE_LICENSE(_license) MODULE_INFO(license, _license)

   #define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)

   #define module_init(x)    __initcall(x);

   #define module_exit(x)    __exitcall(x);

以上代码中,列举了module.h文件中有内核模块的加载、卸载函数的声明,还有一部分宏定义,有的是可有可无的,但是MODULE_LICENSE这个是指定该内核模块的许可证,是必须要有的。

3.1.2.3. 模块加载/卸载函数

3.1.2.3.1. module_init函数

回忆我们使用单片机时,假设我们要使用串口等外设时,是不是都需要调用一个初始化函数, 在这个函数里面,我们初始化了串口的GPIO,配置了串口的相关参数,如波特率,数据位,停止位等等参数。 func_init函数在内核模块中也是做与初始化相关的工作。

内核模块加载函数(位于内核源码/linux/init.h)
static int __init func_init(void)
{
}
module_init(func_init);

返回值:

  • 0: 表示模块初始化成功,并会在/sys/module下新建一个以模块名为名的目录;

  • 非0: 表示模块初始化失败。

在C语言中,static关键字的作用如下:

  1. static修饰的静态局部变量直到程序运行结束以后才释放,延长了局部变量的生命周期。

  2. static的修饰全局变量只能在本文件中访问,不能在其它文件中访问。

  3. static修饰的函数只能在本文件中调用,不能被其他文件调用。

内核模块的代码,实际上是内核代码的一部分, 假如内核模块定义的函数和内核源代码中的某个函数重复了, 编译器就会报错,导致编译失败,因此我们给内核模块的代码加上static修饰符的话, 那么就可以避免这种错误。

__init、__initdata宏定义(位于内核源码/linux/init.h)
1
2
#define __init __section(.init.text) __cold notrace
#define __initdata __section(.init.data)

以上代码 __init、__initdata宏定义(位于内核源码/linux/init.h)中的__init用于修饰函数,__initdata用于修饰变量。 带有__init的修饰符,表示将该函数放到可执行文件的__init节区中,该节区的内容只能用于模块的初始化阶段, 初始化阶段执行完毕之后,这部分的内容就会被释放掉,真可谓是“针尖也要削点铁”。

module_init宏定义
1
#define module_init(x) __initcall(x);

宏定义module_init用于通知内核初始化模块的时候, 要使用哪个函数进行初始化。它会将函数地址加入到相应的节区section中, 这样的话,开机的时候就可以自动加载模块了。

3.1.2.3.2. module_exit函数

理解了模块加载的内容之后,来学习模块卸载函数应该会比较简单。 与内核加载函数相反,内核模块卸载函数func_exit主要是用于释放初始化阶段分配的内存, 分配的设备号等,是初始化过程的逆过程。

内核模块卸载函数
1
2
3
4
static void __exit func_exit(void)
{
}
module_exit(func_exit);

与函数func_init区别在于,该函数的返回值是void类型,且修饰符也不一样, 这里使用的使用__exit,表示将该函数放在可执行文件的__exit节区, 当执行完模块卸载阶段之后,就会自动释放该区域的空间。

__exit、__exitdata宏定义
1
2
#define __exit __section(.exit.text) __exitused __cold notrace
#define __exitdata __section(.exit.data)

类比于模块加载函数,__exit用于修饰函数,__exitdata用于修饰变量。 宏定义module_exit用于告诉内核,当卸载模块时,需要调用哪个函数。

3.1.2.4. 信息打印函数

3.1.2.4.1. printk函数
  • printf:glibc实现的打印函数,工作于用户空间

  • printk:内核模块无法使用glibc库函数,内核自身实现的一个类printf函数,但是需要指定打印等级。

    • #define KERN_EMERG “<0>” 通常是系统崩溃前的信息

    • #define KERN_ALERT “<1>” 需要立即处理的消息

    • #define KERN_CRIT “<2>” 严重情况

    • #define KERN_ERR “<3>” 错误情况

    • #define KERN_WARNING “<4>” 有问题的情况

    • #define KERN_NOTICE “<5>” 注意信息

    • #define KERN_INFO “<6>” 普通消息

    • #define KERN_DEBUG “<7>” 调试信息

查看当前系统printk打印等级:cat /proc/sys/kernel/printk, 从左到右依次对应当前控制台日志级别、默认消息日志级别、 最小的控制台级别、默认控制台日志级别。

1
2
3
4
5
#查看内核打印等级
cat /proc/sys/kernel/printk

#Ubuntu系统信息输出如下
4       4       1       7

如果需要调试信息打印到串口终端需修改内核打印等级:

1
2
#修改内核打印等级
sudo sh -c "echo 7 4 1 7 > /proc/sys/kernel/printk"

使用 dmesg 命令可以打印内核所有打印信息,需注意内核log缓冲区大小有限制,缓冲区数据可能被覆盖掉。

3.1.2.5. 许可证

Linux是一款免费的操作系统,采用了GPL协议,允许用户可以任意修改其源代码。 GPL协议的主要内容是软件产品中即使使用了某个GPL协议产品提供的库, 衍生出一个新产品,该软件产品都必须采用GPL协议,即必须是开源和免费使用的, 可见GPL协议具有传染性。因此,我们可以在Linux使用各种各样的免费软件。 在以后学习Linux的过程中,可能会发现我们安装任何一款软件,从来没有30天试用期或者是要求输入激活码的。

许可证
1
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)

内核模块许可证有 “GPL”,“GPL v2”,“GPL and additional rights”,“Dual SD/GPL”,“Dual MPL/GPL”,“Proprietary”。

3.1.2.6. 相关信息声明

下面介绍一下关于内核模块程序结构的最后一部分内容。 这部分内容只是为了给使用该模块的读者一本“说明书”,属于可有可无的部分, 有则锦上添花,没有也无伤大雅。

内核模块信息声明函数:

函数

作用

MODULE_LICENSE()

表示模块代码接受的软件许可协议,Linux内核遵循GPL V2开源协议,内核模块与linux内核保持一致即可。

MODULE_AUTHOR()

描述模块的作者信息

MODULE_DESCRIPTION()

对模块的简单介绍

MODULE_ALIAS()

给模块设置一个别名

作者信息

内核模块作者宏定义(位于内核源码/linux/module.h)
1
#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)

我们前面使用modinfo中打印出的模块信息中“author”信息便是来自于宏定义MODULE_AUTHOR。 该宏定义用于声明该模块的作者。

模块描述信息

模块描述信息(位于内核源码/linux/module.h)
1
#define MODULE_DESCRIPTION(_description) MODULE_INFO(description, _description)

模块信息中“description”信息则来自宏MODULE_DESCRIPTION,该宏用于描述该模块的功能作用。

模块别名

内核模块别名宏定义(位于内核源码/linux/module.h)
1
#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)

模块信息中“alias”信息来自于宏定义MODULE_ALIAS。该宏定义用于给内核模块起别名。 注意,在使用该模块的别名时,需要将该模块复制到/lib/modules/内核源码/下, 使用命令depmod更新模块的依赖关系,否则的话,Linux内核怎么知道这个模块还有另一个名字。

3.1.3. 实验准备

获取内核模块源码,将配套代码linux_driver解压到内核代码同级目录。

放置例程源码

将驱动示例源码和内核源码放到同级目录后,进入linux_driver/01_helloworld文件夹:

1
2
#进入实验目录
cd linux_driver/01_helloworld

3.1.3.1. Makefile说明

对于内核模块而言,它是属于内核的一段代码,只不过它并不在内核源码中。 为此,我们在编译时需要到内核源码目录下进行编译。 编译内核模块使用的Makefile文件,和我们前面编译C代码使用的Makefile大致相同, 这得益于编译Linux内核所采用的Kbuild系统,因此在编译内核模块时,我们也需要指定环境变量ARCH和CROSS_COMPILE的值。

Makefile (位于 linux_driver/01_helloworld/Makefile)
 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
#指定内核路径,可以是相对路径或绝对路径
KERNEL_DIR=../../kernel/
#KERNEL_DIR=/home/guest/LubanCat_Linux_rk356x_SDK/kernel/

#指定目标架构为arm64
ARCH=arm64

#指定交叉编译工具链的前缀
CROSS_COMPILE=aarch64-linux-gnu-

#导出为环境变量
export  ARCH  CROSS_COMPILE

#指定要编译的内核模块目标文件
obj-m := helloworld.o

#all :默认目标,执行时会编译驱动模块
#$(MAKE) :调用make工具
#-C $(KERNEL_DIR) :指定的内核源码目录
#M=$(CURDIR) :模块的源码位于当前目录
#modules :编译模块
all:
   $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

.PHONE:clean

#清理编译生成的文件
clean:
   $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

3.1.3.2. 编译命令说明

在实验目录linux_driver/01_helloworld下输入如下命令来编译驱动模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#编译内核
make

#信息打印如下
make -C ../../kernel/ M=/home/guest/linux_driver/01_helloworld modules
make[1]: Entering directory '/home/guest/kernel'
CC [M]  /home/guest/linux_driver/01_helloworld/helloworld.o
Building modules, stage 2.
MODPOST 1 modules
CC      /home/guest/linux_driver/01_helloworld/helloworld.mod.o
LD [M]  /home/guest/linux_driver/01_helloworld/helloworld.ko
make[1]: Leaving directory '/home/guest/kernel'

编译成功后,实验目录下会生成名为“helloworld.ko”的驱动模块文件

3.1.4. 内核模块相关命令

我们如愿编译了自己的内核模块,接下来就该了解如何使用这个内核模块了。 将helloworld.ko通过scp或NFS拷贝到开发板中,我们来逐一讲解内核模块加载、卸载以及查看内核模块加载情况的工具。

3.1.4.1. lsmod命令

lsmod列出当前内核中加载的模块,格式化显示在终端,其原理就是将/proc/modules中的信息调整一下格式输出。 lsmod输出列表有一列 Used by, 它表明此模块正在被其他模块使用,显示了模块之间的依赖关系。

3.1.4.2. insmod命令

如果要将一个模块加载到内核中,insmod是最简单的办法, insmod+模块完整路径就能达到目的,前提是你的模块不依赖其他模块,还要注意需要sudo权限。 如果你不确定是否使用到其他模块的符号,你也可以尝试modprobe,后面会有它的详细用法。

通过insmod命令加载helloworld.ko内核模块加载该内核模块的时候, 该内核模块会自动执行module_init()函数,进行初始化操作,该函数打印了 ‘Hello World Module Init’。 再次查看已载入系统的内核模块,我们就会在列表中发现helloworld.ko的身影。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#将helloworld.ko传输到板卡

#在板卡加载内核模块
insmod helloworld.ko

#信息输出如下
[ 6920.611214] helloworld: loading out-of-tree module taints kernel.
[ 6920.611874] Hello World Module Init

#查看内核模块加载列表
lsmod

#信息输出如下
Module                  Size  Used by
helloworld             16384  0

但是有些内核模块有依赖关系,不能直接用insmod加载,需要“前置”依赖模块加载后才能加载。

3.1.4.3. rmmod命令

rmmod工具仅仅是将内核中运行的模块删除,只需要传给它路径就能实现。

rmmod命令卸载某个内核模块时,内核模块会自动执行*_exit()函数,进行清理操作, 我们的helloworld中的*_exit()函数打印了一行内容,如果控制台并没有显示,可以使用dmesg查看, 之所以没有显示是与printk的打印等级有关,前面有关于printk函数有详细讲解。

rmmod不会卸载一个模块所依赖的模块,需要依次卸载。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#在板卡卸载内核模块
rmmod helloworld.ko

#信息输出如下
[ 7962.993271] Hello World Module Exit

#查看内核模块加载列表
lsmod

#信息输出如下
Module                  Size  Used by

3.1.4.4. modinfo命令

modinfo用来显示我们在内核模块中定义的几个宏。 我们可以通过modinfo来查看helloworld,我们从打印的输出信息中,可以了解到,该模块遵循的是GPL协议, 该模块的作者是embedfire,该模块的vermagic等等,而这些信息在模块代码中由相关内核模块信息声明函数声明。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#查看内核模块信息
modinfo helloworld.ko

#信息打印如下
filename:       /home/cat/helloworld.ko
license:        GPL
description:    hello world module
author:         embedfire <embedfire@embedfire.com>
depends:
name:           helloworld
vermagic:       4.19.232 SMP mod_unload aarch64

3.1.4.5. modprobe命令

modprobe和insmod具备同样的功能,同样可以将模块加载到内核中,除此以外modprobe还能检查模块之间的依赖关系, 并且按照顺序加载这些依赖,可以理解为按照顺序多次执行insmod。

3.1.4.6. depmod命令

modprobe是怎么知道一个给定模块所依赖的其他的模块呢?在这个过程中,depmod起到了决定性作用,当执行modprobe时, 它会在模块的安装目录下搜索module.dep文件,这是depmod创建的模块依赖关系的文件。

在Linux系统中,/lib/modules目录通常包含内核相关的模块和配置文件,该文件夹包含了与内核版本号相关文件夹,用来存放的模块和配置信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#查看/lib/modules目录
ls /lib/modules/* -l

#信息输出如下
total 252
lrwxrwxrwx 1 root root    31 Jul 11  2025 build -> /usr/src/linux-headers-4.19.232
drwxr-xr-x 5 root root  4096 Jul 11  2025 kernel
-rw-r--r-- 1 root root 58228 Jul 11  2025 modules.alias
-rw-r--r-- 1 root root 51794 Jul 11  2025 modules.alias.bin
-rw-r--r-- 1 root root 27735 Jul 11  2025 modules.builtin
-rw-r--r-- 1 root root     0 Jul 11  2025 modules.builtin.alias.bin
-rw-r--r-- 1 root root 30242 Jul 11  2025 modules.builtin.bin
-rw-r--r-- 1 root root  5520 Jul 11  2025 modules.dep
-rw-r--r-- 1 root root  8017 Jul 11  2025 modules.dep.bin
-rw-r--r-- 1 root root    94 Jul 11  2025 modules.devname
-rw-r--r-- 1 root root  2900 Jul 11  2025 modules.order
-rw-r--r-- 1 root root    55 Jul 11  2025 modules.softdep
-rw-r--r-- 1 root root 19522 Jul 11  2025 modules.symbols
-rw-r--r-- 1 root root 25711 Jul 11  2025 modules.symbols.bin

以上配置文件或者目录说明如下:

配置文件或文件夹

作用

build

指向当前正在运行的内核源代码的符号链接

kernel

包含编译后的内核模块文件(.ko)

modules.alias

定义模块别名的文件

modules.alias.bin

模块别名文件的二进制缓存版本

modules.builtin

列出了由内核构建的模块(静态连接在内核中)

modules.builtin.bin

由内核构建的模块列表的二进制缓存版本

modules.dep

列出了模块之间的依赖关系

modules.dep.bin

模块依赖关系文件的二进制缓存版本

modules.devname

包含了每个模块设备的名称

modules.order

定义模块加载顺序的文件

modules.symbols

保存导出的符号信息

modules.symbols.bin

导出的符号信息的二进制缓存版本

modules.softdep

包含模块软依赖关系的文件

我们最关心的配置文件是modules.dep,该文件列出了模块之间的依赖关系,当我们执行depmod -a建立模块之间的依赖关系时,就会把依赖关系写入到modules.dep当中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#将内核模块放到内核版本目录的kernel目录下,以4.19.232内核版本为例
mv helloworld.ko /lib/modules/4.19.232/kernel/

#建立依赖关系
depmod -a

#查看依赖
cat /lib/modules/4.19.232/modules.dep | grep helloworld

#加载helloworld模块
modprobe helloworld

#信息打印如下
[ 8375.409521] Hello World Module Init

#卸载helloworld模块
modprobe -r helloworld

#信息输出如下
[ 8800.051428] Hello World Module Exit

3.2. 内核模块传参与符号共享实验

3.2.1. 实验说明

本节实验验证内核模块传参和符号共享。

3.2.1.1. 硬件介绍

本节实验使用Lubancat_RK系列板卡加载和卸载内核模块进行测试。

3.2.2. 实验代码讲解

本章的示例代码目录为: linux_driver/02_parameter

3.2.2.1. 内核模块传参代码讲解

内核模块作为一个可拓展的动态模块,为Linux内核提供了灵活性,但是有时我们需要根据不同的应用场景给内核传递不同的参数, 例如在程序中开启调试模式、设置详细输出模式以及制定与具体模块相关的选项,都可以通过参数的形式来改变模块的行为。

Linux内核提供一个宏来实现模块的参数传递:

module_param函数 (内核源码/include/linux/moduleparam.h)
1
2
3
4
#define module_param(name, type, perm) \\
module_param_named(name, name, type, perm)
#define module_param_array(name, type, nump, perm) \\
module_param_array_named(name, name, type, nump, perm)

以上代码中的module_param函数需要传入三个参数:

  • name: 我们定义的变量名;

  • type: 参数的类型,目前内核支持的参数类型有byte,short,ushort,int,uint,long,ulong,charp,bool,invbool。其中charp表示的是字符指针,bool是布尔类型,其值只能为0或者是1;invbool是反布尔类型,其值也是只能取0或者是1,但是true值表示0,false表示1。变量是char类型时,传参只能是byte,char * 时只能是charp。

  • perm: 表示的是该文件的权限,具体参数值见下表。

文件权限

用户组

标志位

解释

当前用户

S_IRUSR

用户拥有读权限

S_IWUSR

用户拥有写权限

当前用户组

S_IRGRP

当前用户组的其他用户拥有读权限

S_IWUSR

当前用户组的其他用户拥有写权限

其他用户

S_IROTH

其他用户拥有读权限

S_IWOTH

其他用户拥有写权限

上述文件权限唯独没有关于可执行权限的设置,请注意, 该文件不允许它具有可执行权限。如果强行给该参数赋予表示可执行权限的参数值S_IXUGO, 那么最终生成的内核模块在加载时会提示错误,见下图。

参数不可赋予可执行权限

使用EXPORT_SYMBOL宏可以用于向内核导出符号,这样的话,其他模块也可以使用导出的符号。

导出符号
1
2
#define EXPORT_SYMBOL(sym) \\
__EXPORT_SYMBOL(sym, "")
3.2.2.1.1. 示例程序
parameter.c(位于linux_driver/02_parameter/parameter.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>

static int itype=0;
module_param(itype,int,S_IWUSR|S_IRUSR);
MODULE_PARM_DESC(itype,"this is int variable");

static bool btype=0;
module_param(btype,bool,S_IWUSR|S_IRUSR);
MODULE_PARM_DESC(btype,"this is bool variable");

static char ctype=0;
module_param(ctype,byte,S_IWUSR|S_IRUSR);
MODULE_PARM_DESC(ctype,"this is byte variable");


static char *stype=0;
module_param(stype,charp,S_IWUSR|S_IRUSR);
MODULE_PARM_DESC(stype,"this is charp variable");

static int iarr[3] = {0, 1, 2};
module_param_array(iarr, int,NULL, S_IWUSR|S_IRUSR);
MODULE_PARM_DESC(iarr,"this is array of int");


static int __init parameter_init(void)
{
pr_info(KERN_INFO "parameter init!\n");
pr_info(KERN_INFO "itype=%d\n",itype);
pr_info(KERN_INFO "btype=%d\n",btype);
pr_info(KERN_INFO "ctype=%d\n",ctype);
pr_info(KERN_INFO "stype=%s\n",stype);
pr_info("*iarr* parameter: %d, %d, %d\n", iarr[0], iarr[1], iarr[2]);
return 0;
}

static void __exit parameter_exit(void)
{
printk(KERN_INFO "parameter exit!\n");
}

EXPORT_SYMBOL(itype);

int my_add(int a, int b)
{
   return a+b;
}

EXPORT_SYMBOL(my_add);

int my_sub(int a, int b)
{
   return a-b;
}

EXPORT_SYMBOL(my_sub);

module_init(parameter_init);
module_exit(parameter_exit);

MODULE_AUTHOR("embedfire <embedfire@embedfire.com>");
MODULE_DESCRIPTION("parameter module");
MODULE_LICENSE("GPL");
  • 第5-24行:定义整型、布尔型、字节型、字符串指针、整型数组5类变量,通过module_param/module_param_array声明为模块参数并添加描述;

  • 第27-36行:在模块初始化函数中输出所有模块参数的当前值;

  • 第38-41行:定义模块退出函数,打印退出提示;

  • 第43/50/57行:导出变量和自定义函数到内核符号表,供其他模块使用;

  • 第59-64行:完成模块入口/出口注册,声明模块元信息。

calculation.c(位于linux_driver/02_parameter/calculation.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
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>

extern int itype;

int my_add(int a, int b);
int my_sub(int a, int b);

static int __init calculation_init(void)
{
   pr_info(KERN_INFO "calculation init!\n");
   pr_info(KERN_INFO "itype+1 = %d, itype-1 = %d\n", my_add(itype,1), my_sub(itype,1));
   return 0;
}

static void __exit calculation_exit(void)
{
   pr_info(KERN_INFO "calculation exit!\n");
}

module_init(calculation_init);
module_exit(calculation_exit);

MODULE_AUTHOR("embedfire <embedfire@embedfire.com>");
MODULE_DESCRIPTION("calculation module");
MODULE_LICENSE("GPL");

calculation.c中使用extern关键字声明的参数itype,调用my_add()、my_sub()函数进行计算。

3.2.3. 实验准备

获取内核模块源码,将配套代码linux_driver/放到内核代码同级目录,然后进入linux_driver/02_parameter/目录中。

3.2.3.1. Makefile说明

Makefile (位于linux_driver/01_helloworld/Makefile)
 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
#指定内核路径,可以是相对路径或绝对路径
KERNEL_DIR=../../kernel/
#KERNEL_DIR=/home/guest/LubanCat_Linux_rk356x_SDK/kernel/

#指定目标架构为arm64
ARCH=arm64

#指定交叉编译工具链的前缀
CROSS_COMPILE=aarch64-linux-gnu-

#导出为环境变量
export  ARCH  CROSS_COMPILE

#指定要编译的内核模块目标文件
obj-m := parameter.o calculation.o

#all :默认目标,执行时会编译驱动模块
#$(MAKE) :调用make工具
#-C $(KERNEL_DIR) :指定的内核源码目录
#M=$(CURDIR) :模块的源码位于当前目录
#modules :编译模块
all:
   $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

.PHONE:clean

#清理编译生成的文件
clean:
   $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

以上Makefile与上一个实验,只有目标文件不同。

3.2.3.2. 编译命令说明

在实验目录下输入如下命令来编译驱动模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#进入parameter例程源码目录
cd linux_driver/02_parameter

#编译驱动模块
make

#信息输出如下
make -C ../../kernel/ M=/home/guest/linux_driver/02_parameter modules
make[1]: Entering directory '/home/guest/kernel'
CC [M]  /home/guest/linux_driver/02_parameter/parameter.o
CC [M]  /home/guest/linux_driver/02_parameter/calculation.o
Building modules, stage 2.
MODPOST 2 modules
CC      /home/guest/linux_driver/02_parameter/calculation.mod.o
LD [M]  /home/guest/linux_driver/02_parameter/calculation.ko
CC      /home/guest/linux_driver/02_parameter/parameter.mod.o
LD [M]  /home/guest/linux_driver/02_parameter/parameter.ko
make[1]: Leaving directory '/home/guest/kernel'

编译成功后,实验目录下会生成名为“parameter.ko”和“calculation.ko”的驱动模块文件

3.2.4. 程序运行结果

将编译好的parameter.ko和calculation.ko拷贝到开发板中,加载parameter.ko并传参, 这时我们声明的四个变量的值,就是变成了我们赋的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#加载parameter内核模块并传参
sudo insmod parameter.ko itype=123 btype=1 ctype=200 stype=abc

#信息输出如下
[11345.419275] parameter init!
[11345.419378] itype=123
[11345.419390] btype=1
[11345.419399] ctype=200
[11345.419410] stype=abc
[11345.419420] *iarr* parameter: 0, 1, 2

#加载calculation内核模块
sudo insmod calculation.ko

#信息输出如下
[12013.851366] calculation init!
[12013.851496] itype+1 = 124, itype-1 = 122

从以上信息可以看到:

  • 加载parameter.ko打印的变量信息和加载时传入的值一致,而不是驱动源码中默认定义的值,说明参数传递成功。

  • 加载calculation.ko打印的itype+1值就是加载parameter.ko传入的itype=123值+1,打印的itype-1值就是itype=123值-1,说明获取itype变量并调用my_add()、my_sub()函数进行计算成功。

查看向内核导出的符号表:

1
2
3
4
5
6
7
#查看my_add和my_sub符号表
cat /proc/kallsyms | grep my_add
cat /proc/kallsyms | grep my_sub

#信息输出如下
ffffff8001005000 T my_add       [parameter]
ffffff800100502c T my_sub       [parameter]

将内核模块存放板卡到/lib/modules/内核版本/kernel目录,然后执行depmod -a后, 再查看modules.dep配置文件可以发现calculation.ko依赖parameter.ko,并且modules.dep还记录了模块的存放位置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#以4.19.232版本为例
sudo cp parameter.ko /lib/modules/4.19.232/kernel
sudo cp calculation.ko /lib/modules/4.19.232/kernel

#建立依赖关系
sudo depmod -a

#查看依赖
cat /lib/modules/4.19.232/modules.dep | grep calculation

#信息输出如下
kernel/calculation.ko: kernel/parameter.ko

执行modprobe calculation命令自动将会parameter.ko模块加载,然后再加载calculation.ko模块。

 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
34
35
36
37
38
39
40
41
42
43
44
45
#先确保parameter模块卸载再进行验证
sudo rmmod parameter

#加载calculation模块
sudo modprobe calculation

#信息输出如下
[11869.247425] parameter init!
[11869.247538] itype=0
[11869.247556] btype=0
[11869.247566] ctype=0
[11869.247577] stype=(null)
[11869.247588] *iarr* parameter: 0, 1, 2
[11869.249872] calculation init!
[11869.249967] itype+1 = 1, itype-1 = -1

#查看模块加载列表
lsmod

#信息输出如下
Module                  Size  Used by
calculation            16384  0
parameter              16384  1 calculation

#卸载calculation模块会同时卸载parameter模块
sudo modprobe -r calculation

#信息输出如下
[12905.618908] calculation exit!
[12905.635115] parameter exit!

#如果使用insmod直接加载calculation模块,因为依赖关系将无法加载
sudo insmod calculation.ko

#信息输出如下
[13037.363718] calculation: Unknown symbol my_sub (err -2)
[13037.363866] calculation: Unknown symbol itype (err -2)
[13037.363888] calculation: Unknown symbol my_add (err -2)
insmod: ERROR: could not insert module calculation.ko: Unknown symbol in module

#同理,加载parameter和calculation模块后也不能先卸载parameter模块
sudo rmmod parameter

#信息输出如下
rmmod: ERROR: Module parameter is in use by: calculation

如果需要使用modprobe加载时传入parameter的参数,可参考以下方式:

  1. 创建/etc/modules-load.d/<模块名>.conf文件,此处创建/etc/modprobe.d/parameter.conf

  2. 在/etc/modprobe.d/parameter.conf,添加以下内容

1
options parameter itype=123 btype=1 ctype=200 stype=abc
  1. 保存后卸载重装模块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#卸载模块并重新加载
sudo modprobe -r calculation parameter
sudo modprobe calculation

#信息输出如下
[ 1262.687687] parameter init!
[ 1262.687803] itype=123
[ 1262.687816] btype=1
[ 1262.687825] ctype=200
[ 1262.687836] stype=abc
[ 1262.687845] *iarr* parameter: 0, 1, 2
[ 1262.689831] calculation init!
[ 1262.690075] itype+1 = 124, itype-1 = 122

3.3. 系统自动加载模块

我们自己编写了一个模块,或者说怎样让它在板子开机自动加载呢? 这里就需要用到上述的depmod和modprobe工具了。

首先需要将我们想要自动加载的模块统一放到“/lib/modules/内核版本/kernel”目录下,内核版本使用 uname -r 命令查询; 其次使用depmod建立模块之间的依赖关系,使用命令 depmod -a ; 这个时候我们就可以在modules.dep中看到模块依赖关系。

以4.19.232内核版本,helloworld.ko为例:

1
2
3
4
5
6
7
8
#1.拷贝模块到内核目录
sudo cp helloworld.ko /lib/modules/4.19.232/kernel/

#2.建立依赖关系
sudo depmod -a

#3.查看依赖关系
cat /lib/modules/4.19.232/modules.dep | grep helloworld

最后在/etc/modules或者创建/etc/modules-load.d/<模块名>.conf中加上我们自己的模块,注意在该配置文件中, 模块 不写成.ko结尾 代表该模块与内核紧耦合。

如修改/etc/modules,添加以下内容

1
helloworld

然后重启开发板,执行lsmod就能查看到我们的模块开机就被加载到内核里面了。

提示

除了本章节介绍的方式,通过系统自启动服务或脚本直接调用insmod或modprobe命令来加载驱动模块也是十分方便的。