本文为笔者学习阶段的一个记录,多有错漏与描述不清之处,欢迎大家批评指正。
文章目录
本文是Linux驱动编程中的前置基础知识
一、模块简介
1.模块
在编译内核时,若打开内核的图形化配置界面,可以看到非常多的配置项,对于一个配置,我们可以选择将其编译进内核,也可以选择将其编译成模块。
编译进内核的优点在于性能较高,并且开机自启,不需要手动安装,而且安全性比较高,因为它不能被修改。而编译成模块则是运用较为灵活,可以根据需求是否安装模块,并且不会占用内核空间,最主要的是它便于维护,不需要每次改动都编译整个内核。
2.根文件系统中的模块
通常 Linux 的模块都存放于 /lib/modules/xxx/ 目录,其中 xxx 为 linux 内核的版本号,例如示例中的开发板使用 3.10.65 版本的内核,因此模块存放的路径就为 /lib/modules/3.10.65/,进入这个目录可以看到当前系统中所存在的模块:
3.模块编译
编译 linux 模块时,必须依赖于一份已经编译过的内核源码,随后编写一个简单 makefile,该 makefile 通过将工作目录切换至源码目录,并借助源码目录中的顶层 makefile 的 make 命令并追加 modules 选项将我们指定的源文件编译成模块。makefile示例见本文实验源码处。
4.模块的加载与卸载
加载与卸载模块分别使用 insmod 和 rmmod 命令,这两个命令使用非常简单,直接使用命令名+模块名即可使用。
此外 modpro 命令也可用于安装与卸载模块,该命令会自动寻找当前模块所依赖的其他模块随后将它们一起卸载或安装。当 modpro 安装一个新的模块时,通常会出现错误:
这时候我们需要先使用一次 depmod 函数,然后再次使用 modpro 即可。第一次使用 depmod 可能出现以下错误:
可以看到报错缺少两个文件,我们使用 touch 创建它们即可:
touch modules.order
touch modules.builtin
二、编写一个模块
1.基本框架
模块的安装和卸载是模块编程中最基本而且也是必须实现的。一个最基本的模块框架如下:
#include <linux/module.h>
#include <linux/init.h>
static int __init led_init(void) {
/* code */
return 0;
}
static void __exit led_exit(void) {
/* code */
}
/* 模块安装时会触发此处传入的函数 */
module_init(led_init);
/* 模块卸载时会触发此处传入的函数 */
module_exit(led_exit);
/* 许可证 */
MODULE_LICENSE("GPL V2");
当模块安装时,module_init 函数会被触发并调用函数指针 led_init,通常在该函数指针中初始化设备。卸载时也是同理。
最后一行的 MODULE_LICENSE 语句用于添加许可证,许可证用于声明描述内核模块的许可权限,在 Linux 内核模块领域,通常传入 GPLv2 或 GPL 即可。如果不声明则在编译时会出现如下警告:
模块被加载时,将收到内核被污染(kerneltainted)的警告:
2.安装时向模块传参
在使用 insmod 或 modpro 命令安装模块时,可以在命令后面追加相应参数传入模块。当传递多个参数时,每个参数之间使用空格隔开;传递数组时,数组中的每一个成员应当使用逗号隔开。使用示例如下:
#传递普通参数时:
modprobe prints a=2 b=6
#传递数组时:
modprobe prints a=2 b=6 array=7,8,9
#传递字符串时:
modprobe prints c="78"
3.模块内部接收传参
模块内通常使用 module_param 或者 module_param_array 函数接收外部的传参,module_param 用于接收普通变量和字符串,而 module_param_array 则用于接收数组,函数原型如下:
module_param(name, type, perm)
module_param_array(name, type, nump, perm)
name:变量名。传入变量或数组之前,应该先全局定义这个变量或数组。
type:变量类型,可选值见下表。
nump:通常先定义一个变量然后传入其地址,对应实际传入数组的成员数量。
perm:该变量在路径/sys/module/模块名/parameters/下与之对应的文件权限。通常传入0444或者宏S_IRUGO。
tpye 变量可选值如下表:
可选值 | 描述 | 可选值 | 描述 |
---|---|---|---|
int | int类型 | uint | unsigned int类型 |
short | short类型 | ushort | unsigned short 类型 |
long | long类型 | ulong | unsigned long 类型 |
invbool | 反转的bool值 | bool | bool类型 |
charp | 字符串 |
使用示例如下:
//例如,接收int类型变量时:
static int a = 10;
module_param(a, int, 0664);
//例如,接收字符串时:
static char * c = "hello";
module_param(c, charp, 0664);
//例如,接收数组时:
static int num;
static int array[3] = {1,2,3};
module_param_array(array, int, &num, S_IRUGO);
4.增加传参的描述信息
为了方便使用者了解参数的含义。使用 MODULE_PARM_DESC 函数就可以提供对参数的描述:
MODULE_PARM_DESC(_parm, desc)
parm:参数的变量名。
desc:描述信息,应当传入一个字符串。
例如,描述变量 a 代表着 money:
 MODULE_PARM_DESC(a, "this is money !!!");
当模块中使用该语句后,在终端中使用 modinfo 命令查看模块信息时就可以看到提示语句 “this is money !!!” :
通过这张图可以看到,模块其实对每一个参数都有着默认的描述,默认描述中提示用户该参数是什么类型。
5.导出一个函数或变量
在编写驱动时,通常会在全局变量和函数前使用修饰符 static,这时候如果外部的模块想引用这个变量或者函数,就需要使用函数 EXPORT_SYMBOL,该函数用于导出一个变量或函数,其原型如下:
EXPORT_SYMBOL(sym)
sym:要导出的函数名或变量名。
例如,编写一个函数 add 并定义两个变量 one 与 tow ,随后导出 add 函数和两个变量供外部模块调用,代码如下:
static int one = 4;
static int two = 5;
static int add(int a , int b) {
return a + b;
}
EXPORT_SYMBOL(add);
EXPORT_SYMBOL(one);
EXPORT_SYMBOL(two);
此时,在其他模块中便可以使用 extern 关键字来声明这个外部函数以及两个外部变量进行调用,代码如下:
extern int add(int a, int b);
extern int one;
extern int two;
static int __init prints_init(void) {
pr_emerg("%d\r\n", add(one, two));
return 0;
}
三、实验程序
1.实验简述
在本节模块编程实验中搭建了两个最基本的模块框架,分别为 print 和 add 。
add 模块中导出了 add 函数以及两个全局变量。
print 模块则调用 add 函数并传入两个 add 模块中的全局变量,随后打印出add函数的返回值。
print 模块还会将安装该模块时传入的参数打印出来。
2.程序源码
print.c
#include "linux/kern_levels.h"
#include "linux/moduleparam.h"
#include "linux/printk.h"
#include <linux/module.h>
#include <linux/init.h>
/* 接受外部传参 a=? ,传入的新值会覆盖掉原有的值 */
static int a = 10;
module_param(a, int, 0664);
/* 接受外部传参 b=? ,传入的新值会覆盖掉原有的值 */
static int b = 20;
module_param(b, int, 0664);
/* 接受外部传参 str="?" ,传入的新值会覆盖掉原有的值 */
static char * str = "hello";
module_param(str, charp, 0664);
/* 接受外部传参 array=?,?,? ,传入的新值会覆盖掉原有的值 */
/* num保存着外部传入的数组成员数量 */
static int num;
static int array[3] = {1,2,3};
module_param_array(array, int, &num, S_IRUGO);
/* 增加在modinfo时对参数a的描述 */
MODULE_PARM_DESC(a, "this is money !!!");
/* 声明外部变量 */
extern int one;
extern int two;
/* 声明外部函数 */
extern int add(int a, int b);
/* 模块加载时打印各传入参数与外部参数 */
static int __init prints_init(void) {
pr_emerg("%d\r\n", add(one, two));
pr_emerg("a = %d, b = %d, str = %s\n", a, b, str);
for(; num >= 0; num --) {
printk(KERN_EMERG"array[%d] = %d\n", num, array[num]);
}
pr_emerg("extern one = %d, two = %d\n", one, two);
return 0;
}
/* 模块卸载时打印提示 */
static void __exit prints_exit(void)
{
printk(KERN_EMERG"prints rmmod\r\n");
}
/* 模块的入口与出口函数 */
module_init(prints_init);
module_exit(prints_exit);
/* 许可证 */
MODULE_LICENSE("GPL V2");
/* 作者 */
MODULE_AUTHOR("zzh");
add.c
#include "linux/export.h"
#include "linux/module.h"
/* 定义静态全局变量与静态函数 */
static int one = 4;
static int two = 5;
static int add(int a , int b) {
return a + b;
}
/* 导出这些静态全局变量与静态函数 */
EXPORT_SYMBOL(add);
EXPORT_SYMBOL(one);
EXPORT_SYMBOL(two);
/* 模块安装时打印提示 */
static int __init add_init(void)
{
printk(KERN_EMERG"insmod add\r\n");
return 0;
}
/* 模块卸载时打印提示 */
static void __exit add_exit(void)
{
printk(KERN_ERR"add remove\r\n");
}
/* 模块的入口和出口函数 */
module_init(add_init);
module_exit(add_exit);
/* 许可证 */
MODULE_LICENSE("GPL V2");
/* 作者 */
MODULE_AUTHOR("zzh");
makefile
#最终生成的文件,这里叫什么.o就会生成什么.ko
obj-m := prints.o adds.o
#每个模块所依赖的源文件
prints-objs := print.o
adds-objs := add.o
#内核源码路径
KERNELDIR := /home/zzh/linux/source/lichee/linux-3.10
#架构以及交叉编译器
FLAG := ARCH=arm64 CROSS_COMPILE=aarch64-linux-
build:kernel_modules
kernel_modules:
@make $(FLAG) -C $(KERNELDIR) M=$(PWD) modules
@rm -fr *.o *.mod.c *.bak *.order *.symvers .*.cmd rm .tmp_versions*
clean:
@make $(FLAG) -C $(KERNELDIR) M=$(PWD) clean
3.实验过程与现象
使用modpro安装prints.ko:
modprobe prints a=3 b=6 str="hello" array=99,66,88
可以看到以下输出:
若使用modinfo查看模块信息,就可以看到对各个参数的描述以及驱动信息。