参考书籍《Linux设备驱动第三版》
一、源码解析
--hello_module.c--
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
{
printk(KERN_ALERT "Hello, world.\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
1、2行,moudle.h 包含了大量加载模块需要的函数和符号的定义;init.h 指定你的初始化和清理函数;有时候还需要包含moudleparam.h, 使得可以在模块加载时传递参数给模块(参考第四部分)。
这里用到了当前内核源码树的位置,这也是在开始Linux内核开发之前必需的环境。本文使用的内核为安装在VMware上的Ubuntu14.04LTS,可以通过在终端中输入
% uname -r获取内核版本,楼主版本号为 4.4.0-91-generic
[Linux版本命名 4.x 中x若为偶数表示稳定版本,x为奇数表示测试发行版,如果想自己下载内核并自行编译构造内核源码树的话,建议下载稳定版本]
15、16行调用特殊的内核宏,module_init连接加载函数,对应模块加载(insmod)过程;module_exit连接卸载函数,对应模块卸载(rmmod)过程。module_init是必须的,因为我们要加载该模块;module_exit则可有可无,但在没有定义该函数的情况下该模块无法卸载。
4~8行为hello_init定义部分:
初始化函数应当声明成静态的, 因为它们只在当前文件可见;
printk 函数在 Linux 内核中定义并且对模块可用; 它与标准 C 库函数 printf 的行为相似. 内核需要它自己的打印函数, 因为它靠自己运行, 没有 C 库的帮助。模块能够调用 printk 是因为, 在 insmod 加载了它之后, 模块被连接到内核并且可存取内核的公用符号。
KERN_ALERT 为消息的优先级的宏定义。
#define KERN_ALERT "<1>"
其它还有 0~7,数值越小,优先级越高。
10~13行为hello_exit清理函数定义部分,当模块从系统中卸载时,它负责回收之前模块申请的资源(这里并没有申请),清理函数没有返回值, 因此它被声明为 void。
18行 MODULE_LICENSE 告知内核该模块带有一个自由的许可证,Linux通常都是遵守GPL授权的,这里至少应该是 MODULE_LICENSE("GPL") ,内核中的模块必须满足内核的授权权限。类似的还有
MODULE_AUTHOR(author);
MODULE_DESCRIPTION(description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);
二、Makefile解析
--Makefile--
KERN_VER = $(shell uname -r)
KERN_DIR = /lib/modules/$(KERN_VER)/build
obj-m += hello_module.o
all:
make -C $(KERN_DIR) M=`pwd` modules
.PHONY: clean
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
第1行,获取内核版本信息
第2行,获取内核源码树位置信息
第4行,将hello_module.c编译成一个模块(m—模块,y-静态链接到zImage中,-n表示不编译)
这一行是必不可少的,实际上Makefile也只需要这一行就能搞定。假如有个新的模块module.ko,它依赖于两个文件file1.c和file2.c的话,这一行应该改为:
obj-m +=module.o
module-objs := file1.o file2.o
6、7行,all 目标为 make modules 表示编译为模块-C 表示进入到指定目录下借用内核源码的体系进行编译链接
M=`pwd`把当前路径记录到M中,编译完成之后拷贝回M
第9行,指定clean为伪目标,其目的有二,其一是防止目录下有同名文件(文件名为clean),其二是保证clean目标最新,每次都会重新解释。
10、11行,clean掉拷贝过来的文件
编译模块
% make
可以使用以下命令查看模块信息
% modinfo xxx.ko
其中的vermagic之中就记录了版本信息和处理器体系,它们必须与当前的系统的版本信息一致
三、测试
1、先将系统切换到超级用户
% su
2、安装前先查看系统已有模块
% lsmod
3、安装模块
% insmod xxx.ko
install module.当我们的模块引用了当前内核中没有定义的符号或者没有安装的模块时,也可以使用命令 %modprobe来加载。照理shell应该打印 “Hello, world.” 这条信息,这里由于Ubuntu将其保存在系统日志中,并没有实时打印出来,所以我们看不到,使用命令
% dmesg
可以打开日志文件。模块是添加到链表头的
4、卸载模块
% rmmod xxx
remove module,同理使用dmesg可以显示卸载信息
卸载之后,hello_module已经不在系统模块里面了。
四、模块参数
当我们希望在使用insmod 或者 modprobe 加载驱动时附带参数的时候,可以用 "module_param(参数名, 参数类型, 参数读写权限) " 为模块定义一个参数(modprobe 也可以从它的配置文件(/etc/modprobe.conf)读取参数的值),此时的命令格式为
% insmode(或modprobe)模块名 参数名1=参数值 参数名2=参数值 …
如果没有输入参数值,则使用文件中定义的默认值。
其中的,参数类型可以是byte, short, ushort, int, uint, long, ulong, charp(字符指针), bool或invbool(布尔取反); 参数读/写权限,应当使用在<linux/stat.h> 中定义的值,具体定义参考该文件,这里使用值 “S_IRUGO”。
除此之外,模块也可以拥有参数数组,形式为“module_param_array(数组名,数组类型,数组长,参数读/写权限)"。从2.6.0~2.6.10版本,需将数组长变量名赋给”数组长“,从2.6.10版本开始,需将数组长变量的指针赋给”数组长“,当不需要保存实际输入的数组元素个数时,可以设置”数组长“为NULL。
运行insmod/modprobe命令时,应使用逗号分隔输入的数组元素。
更改后的 hello_module.c
#include <linux/init.h>
#include <linux/module.h>
static char *name = "xxx";
static uint num = 1;
static int __init hello_init(void)
{
printk(KERN_ALERT "num = %d. Hello, %s.\n", num, name);
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world.\n");
}
module_init(hello_init);
module_exit(hello_exit);
module_param(name, charp, S_IRUGO);
module_param(num, uint, S_IRUGO);
MODULE_LICENSE("Dual BSD/GPL");
增加了4、5、20、21行,第9行稍作修改,用以显示参数值。
重现make,加载
% insmod hello_module.ko
这里先没有输入参数值,只打印日志中最后一行
% dmesg | tail -1
卸载,重新加载时携带参数值
% insmod hello_module.ko num=5 name=xiaoming
当然,也可以只传入一个参数
五、模块调用
1、复制当前驱动到ch2p文件夹中
2、修改hello_module.c为hello_module2.c,在里面添加如下4条语句(其余不变)
void fun(void)
{
printk("come from ch2p.\n");
}
EXPORT_SYMBOL(fun);
3、修改Makefile文件中对应语句
obj-m += hello_module2.o
4、make,之后 insmod hello_module2.ko
5、修改ch2文件夹下的hello_module.c
extern void fun(void);
static int __init hello_init(void)
{
fun();
printk(KERN_ALERT "num = %d. Hello, %s.\n", num, name);
return 0;
}
相当于添加了两行 extern void fun(void); 和 fun(); 其余不变。
6、需要注意的是,这里直接make是找不到fun函数的。这里有三种方法可以解决,参考:http://blog.csdn.net/xhz1234/article/details/44278137
总结来说,
调用时,被调用者通过EXPORT_SYMBOL或EXPORT_SYMBOL_GPL导出符号;调用者通过extern声明该符号。
加载时,先加载被调用者所在模块,再加载调用者所在模块。
卸载时,先卸载调用者所在模块,再卸载被调用者所在模块。