掘金 后端 ( ) • 2024-04-23 11:02

关注微信公众号: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内核模块子系统提供了相应的起始点和结束点函数钩子:

  1. Init Function
  2. 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()支持的日志等级,从高到低如下所示:

  1. KERN_EMERGE:最紧急的消息,通常用在内核crash之前;
  2. KERN_ALERT:用于需要立即采取相应动作的场景;
  3. KERN_CRIT:通常与严重的硬件或者软件错误关联;
  4. KERN_ERR:用于报告错误条件,设备驱动通常用KERN_ERR来报告硬件设备运行过程中相关的错误;
  5. KERN_WARNING:用于问题告警的场景,但通常不会产生严重的问题;
  6. KERN_NOTICE:一些普遍但是值得注意的场景,大部分安全相关的问题都使用该日志等级;
  7. KERN_INFO:用于提供系统运行信息的消息,很多设备驱动程序使用该日志等级在启动阶段报告硬件的信息。
  8. KERN_DEBUG:用作开发调试目的的日志消息。

在比较新的版本中,Linux内核提供了如下API来替代直接使用printk():

  1. pr_info —— KERN_INFO;
  2. pr_cont:追加到与前一个日志消息同一行;
  3. pr_debug —— KERN_DEBUG;
  4. pr_err —— KERN_ERR;
  5. pr_warn —— KERN_WARNING。

示例:

 printk(KERN_INFO "Welcome to my first simple linux device driver!");

printk vs printf:

  1. printk()是内核级函数,能够打印多种预定义等级的日志,可以通过dmesg来获取其输出;
  2. 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来查看内核模块的详细信息。

image-20240422232432917

关注微信公众号:Linux内核拾遗

文章来源:https://mp.weixin.qq.com/s/aId6hhShCFaBfphyr9SLLw