关注微信公众号:Linux内核拾遗
文章来源:https://mp.weixin.qq.com/s/aId6hhShCFaBfphyr9SLLw
1 内核模块信息介绍
一个Linux内核模块通常包含以下描述信息,这些描述信息可以使用"linux/module.h"中提供的宏来定义。
1.1 License
模块的许可证信息,表示该模块的使用和分发受到的许可限制。常见的许可证包括GPL(GNU通用公共许可证)、LGPL(GNU宽通用公共许可证)、BSD许可证等。
使用MODULE_LICENSE来声明:
MODULE_LICENSE("GPL");
MODULE_LICENSE("GPL v2");
MODULE_LICENSE("Dual BSD/GPL");
1.2 Author
模块的作者或维护者信息,用于指明该模块的创作者或负责人。
使用MODULE_AUTHOR来声明,允许声明多次:
MODULE_AUTHOR("Author1");
MODULE_AUTHOR("Author2");
1.3 Module Description
简要描述该模块的功能、作用以及用途,使用户能够快速了解模块的主要功能和特点。
使用MODULE_DESCRIPTION来声明:
MODULE_DESCRIPTION("The first simple linux device driver");
1.4 Module Version
模块的版本号,用于标识模块的不同版本。通常使用数字和可选的附加信息(如alpha、beta、rc等)来表示模块的版本信息。
使用MODULE_VERSION进行声明,通常使用的格式是:[<epoch>:]<version>[-extra-version]
。
MODULE_VERSION("2:1.0");
2 内核模块编程入门
和普通的应用程序一样,内核模块也有相应的“main函数”,它作为内核模块代码执行的起始点。此外,内核模块还有对应结束点,用于在内核模块退出的时候执行代码。
Linux内核模块子系统提供了相应的起始点和结束点函数钩子:
- Init Function
- Exit Function
内核模块编写者通过实现模块自身的起始点和结束点函数,并在内核模块加载的时候注册到内核函数中,Linux内核模块子系统将在加载(insmod)和卸载(rmmod)内核模块的时候调用相应的钩子函数实现,用于完成内核模块的初始化或者资源清理回收等工作。
需要注意的是,内核模块代码不能调用任何用户空间的代码库、API或者系统调用,它只能引用Linux内核提供的头文件。
2.1 初始化/退出函数
一个最简单的内核模块初始化函数如下:
static int __init init_func(void)
{
return 0;
}
module_init(init_func);
void __exit exit_func(void)
{
}
module_exit(exit_func);
其中module_init和module_exit宏分别用于将初始化函数init_func和退出函数exit_func注册到Linux内核模块子系统中。
2.2 打印函数
打印函数是Linux内核模块中最有效的调试手段和工具,类似于C语言中的printf(),Linux内核提供了内核空间版本的打印函数printk()。
在使用上,两者最大的不同在于,开发者可以根据日志消息的重要性程度或者优先级,在printk()中指定起日志等级,其中日志等级是通过宏来声明的。printk()支持的日志等级,从高到低如下所示:
- KERN_EMERGE:最紧急的消息,通常用在内核crash之前;
- KERN_ALERT:用于需要立即采取相应动作的场景;
- KERN_CRIT:通常与严重的硬件或者软件错误关联;
- KERN_ERR:用于报告错误条件,设备驱动通常用KERN_ERR来报告硬件设备运行过程中相关的错误;
- KERN_WARNING:用于问题告警的场景,但通常不会产生严重的问题;
- KERN_NOTICE:一些普遍但是值得注意的场景,大部分安全相关的问题都使用该日志等级;
- KERN_INFO:用于提供系统运行信息的消息,很多设备驱动程序使用该日志等级在启动阶段报告硬件的信息。
- KERN_DEBUG:用作开发调试目的的日志消息。
在比较新的版本中,Linux内核提供了如下API来替代直接使用printk():
- pr_info —— KERN_INFO;
- pr_cont:追加到与前一个日志消息同一行;
- pr_debug —— KERN_DEBUG;
- pr_err —— KERN_ERR;
- pr_warn —— KERN_WARNING。
示例:
printk(KERN_INFO "Welcome to my first simple linux device driver!");
printk vs printf:
- printk()是内核级函数,能够打印多种预定义等级的日志,可以通过dmesg来获取其输出;
- printf()是用户空间函数,通常输出到一个文件描述符中,例如stdout,其输出可以在stdout控制台获取。
3 一个完整的Linux设备驱动示例
下面给出了一个完整的Linux设备驱动程序源代码:
my_driver.c
#include<linux/kernel.h>
#include<linux/init.h>
#include<linux/module.h>
static int __init init_func(void)
{
printk(KERN_INFO "Welcome to my first simple linux device driver\n");
printk(KERN_INFO "Kernel Module Inserted\n");
return 0;
}
static void __exit exit_func(void)
{
printk(KERN_INFO "Kernel Module Removed\n");
}
module_init(init_func);
module_exit(exit_func);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FeiFei <[email protected]>");
MODULE_DESCRIPTION("A simple linux kernel driver");
MODULE_VERSION("2:1.0");
Makefile
obj-m += my_driver.o
KDIR = /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(shell pwd) modules
clean:
make -C $(KDIR) M=$(shell pwd) clean
将这两个文件放到同一个目录下,运行"make"进行编译得到内核模块文件:my_driver.ko,最后可以通过insmod/rmmod来加载或者卸载我们编译好的内核模块,通过dmesg来查看内核模块的日志输出,也可以通过modinfo my_driver.ko来查看内核模块的详细信息。
关注微信公众号:Linux内核拾遗
文章来源:https://mp.weixin.qq.com/s/aId6hhShCFaBfphyr9SLLw