By: Ailson Jack
Date: 2021.05.09
个人博客:http://www.only2fire.com/
本文在我博客的地址是:http://www.only2fire.com/archives/134.html,排版更好,便于学习,也可以去我博客逛逛,兴许有你想要的内容呢。
因为Linux 操作系统采用了宏内核结构,宏内核的优点是执行效率非常高,但缺点也是十分明显的,一旦我们想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核。为了解决这一缺点,Linux 中引入了内核模块这一机制。
内核模块就是实现了某个功能的一段内核代码,在内核运行过程,可以加载这部分代码到内核中,从而动态地增加了内核的功能。基于这种特性,我们进行设备驱动开发时,以内核模块的形式编写设备驱动,只需要编译相关的驱动代码即可,无需对整个内核进行编译。内核模块的引入不仅提高了系统的灵活性,对于开发人员来说更是提供了极大的方便。
内核模块定义:内核模块全称 Loadable Kernel Module(LKM),是一种在内核运行时加载一组目标代码来实现某个特定功能的机制。
内核模块特点:
模块本身不被编译入内核映像,这控制了内核的大小;
模块一旦被加载,它就和内核中的其它部分完全一样。
我们编写的内核模块,经过编译,最终形成以.ko为后缀的文件。ko 文件在数据组织形式上是 ELF(Excutable And Linking Format) 格式,是一种普通的可重定位目标文件。
对于程序入门学习来说,Hello World程序是经典的例子,这里我们也实现一个简单的Hello内核模块用于了解内核模块编程的基本框架。
hello_module.c文件的内容如下所示:
/** * @file hello_module.c * @author Ailson Jack (jackailson@foxmail.com) * @brief * @version 1.0 * @date 2021-05-08 * * @copyright Copyright (c) 2021 * * @note blog:www.only2fire.com * */ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> /* 内核模块加载函数 */ static int __init hello_module_init(void) { printk(KERN_EMERG "[KERN_EMERG] Hello Module init!\r\n"); printk("[default] Hello Module init!\r\n"); return 0; } /* 内核模块卸载函数 */ static void __exit hello_module_exit(void) { printk(KERN_EMERG "[KERN_EMERG] Hello Module exit!\r\n"); printk("[default] Hello Module exit!\r\n"); } module_init(hello_module_init); module_exit(hello_module_exit); MODULE_LICENSE("GPL v2"); //表示模块代码接受的软件许可协议 MODULE_AUTHOR("Ailson Jack"); //描述模块的作者信息 MODULE_DESCRIPTION("hello module"); //对模块的简单介绍 MODULE_ALIAS("test_module"); //给模块设置一个别名
Linux 内核模块的代码框架通常由下面几个部分组成:
模块加载函数 (必须):当通过 insmod 或 modprobe 命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。
模块卸载函数 (必须):当执行 rmmod 命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。
模块许可证声明 (必须):许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。
模块参数:模块参数是模块被加载时,可以传值给模块中的参数。
模块导出符号:模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。
模块的其他相关信息:可以声明模块作者等信息。
Hello内核模块中,使用3个头文件,下面说说这3个头文件具体提供的信息:
#include <linux/module.h>:包含内核模块信息声明的相关函数;
#include <linux/init.h>:包含了 module_init() 和 module_exit() 函数的声明;
#include <linux/kernel.h>: 包含内核提供的各种函数,如 printk。
module_init():声明内核模块加载函数,加载内核模块的时候会调用声明的内核模块加载函数,模块加载成功,会在**/sys/module**下新建一个以模块名为名的目录。
module_exit():声明内核模块卸载函数,卸载内核模块的时候会调用声明的内核模块卸载函数。
__init 用于修饰函数, __initdata 用于修饰变量。带有 __init 的修饰符,表示将该函数放到可执行文件的 __init 节区中,该节区的内容只能用于模块的初始化阶段,初始化阶段执行完毕之后,这部分的内容就会被释放掉,真可谓是“针尖也要削点铁”。
__exit 用于修饰函数,__exitdata 用于修饰变量。带有__exit的修饰符,表示将该函数放到可执行文件的__exit节区,当执行完模块卸载阶段之后,就会自动释放该区域的空间。
注意:hello_module_init()函数的返回值是int,hello_module_exit()的返回值是void,并且这两个函数都使用static进行修饰,表示函数只能在本文件进行调用,不能被其他文件调用。
printk函数的打印等级:
#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函数可以指定打印等级,当不指定打印等级的时候,会使用默认的打印等级。
查看当前系统 printk 打印等级: cat /proc/sys/kernel/printk,从左到右依次对应控制台日志级别、默认消息日志级别、最小的控制台日志级别、默认控制台日志级别。
控制台日志级别:优先级高于该值得消息将被打印到到控制台;
默认消息日志级别:将用该优先级来打印没有指定优先级的消息;
最小的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级);
默认控制台日志级别:控制台日志级别的缺省值。
以上的数值设置,数值越小,优先级越高。
假设你想让hello_module_init()或者hello_module_exit()函数中,没有指定打印等级的printk的内容输出到控制台,那么你可以将"默认消息日志级别"设置为小于4,可以设置为3(只需要数值小于控制台日志级别即可),执行的命令如下:
sudo sh -c "echo '4 3 1 7' > /proc/sys/kernel/printk"
然后执行加载或者卸载模块,就可以看到未指定打印等级的消息输出到控制台了。
查看内核所有打印信息: dmesg,注意内核 log 缓冲区大小有限制,缓冲区数据可能被覆盖掉。
对于内核模块而言,它是属于内核的一段代码,只不过它并不在内核源码中。为此,我们在编译时需要到内核源码目录下进行编译。编译内核模块使用的 Makefile 文件,和我们前面编译 C 代码使用的 Makefile 大致相同,这得益于编译 Linux 内核所采用的 Kbuild 系统,因此在编译内核模块时,我们也需要指定环境变量 ARCH 和CROSS_COMPILE 的值。
编译Hello内核模块使用的Makefile文件内容如下:
# 指向编译出来的 linux 内核具体路径 KERNEL_DIR = ../kernel/ebf-buster-linux/build_image/build # 定义变量,并且导出变量给子 Makefile 使用 ARCH = arm CROSS_COMPILE = arm-linux-gnueabihf- export ARCH CROSS_COMPILE # obj-m := <模块名>.o: 定义要生成的模块 obj-m := hello_module.o # 选项 "-C":让 make 工具跳转到 linux 内核目录下读取顶层 Makefile # "M=" 表示内核模块源码目录 # $(CURDIR): Makefile 默认变量,值为当前目录所在路径 # make modules: 执行 Linux 顶层 Makefile 的伪目标,它实现内核模块的源码读取并编译为.ko文件 all: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules .PHONY:clean copy clean: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean copy: cp *.ko /home/ailsonjack/share/nfs/temp
在内核模块的目录中,执行make命令编译内核模块,生成hello_module.ko文件,将hello_module.ko文件通过nfs或者scp拷贝到开发板,即可加载该内核模块。
lsmod 列出当前内核中的所有模块,格式化显示在终端,其原理就是将/proc/module 中的信息调整一下格式输出。 lsmod 输出列表有一列 Used by,它表明此模块正在被其他模块使用,显示了模块之间的依赖关系。
如果要将一个模块加载到内核中, insmod 是最简单的办法, insmod+模块完整路径就能达到目的,前提是你的模块不依赖其他模块,还要注意需要 sudo 权限。如果你不确定是否使用到其他模块的符号,你也可以尝试modprobe,后面会有它的详细用法。
rmod 工具仅仅是将内核中运行的模块删除,只需要传给它路径就能实现。
rmmod 不会卸载一个模块所依赖的模块,需要依次卸载,当然用 modprobe -r 可以一键卸载。
modprobe 和 insmod 具备同样的功能,同样可以将模块加载到内核中,除此以外 modprobe 还能检查模块之间的依赖关系,并且按照顺序加载这些依赖,可以理解为按照顺序多次执行 insmod。
modprobe 是怎么知道一个给定模块所依赖的其他的模块呢?在这个过程中, depend 起到了决定性作用,当执行 modprobe 时,它会在模块的安装目录下搜索 module.dep 文件,这是 depmod 创建的模块依赖关系的文件。
modinfo 用来显示内核模块一些信息。比如:modinfo hello_module.ko
我们自己编写了一个模块,或者说怎样让它在板子开机自动加载呢?这里就需要用到上述的 depmod 和 modprobe 工具了。
首先需要将我们想要自动加载的模块统一放到”/lib/modules/内核版本”目录下,内核版本使用 'uname -r’查询;其次使用 depmod 建立模块之间的依赖关系,命令’ depmod -a’;这个时候我们就可以在 modules.dep 中看到模块依赖关系。
最后在/etc/modules 加上我们自己的模块,注意在该配置文件中,模块不写成.ko 形式代表该模块与内核紧耦合,有些是系统必须要跟内核紧耦合,比如 mm 子系统,一般写成.ko 形式比较好,如果出现错误不会导致内核出现 panic 错误,如果集成到内核,出错了就会出现panic。
欢迎关注博主的公众号呀:
如果文中有什么问题欢迎指正,毕竟博主的水平有限。
如果这篇文章对你有帮助,记得点赞和关注博主就行了^_^。
排版更好的内容见我博客的地址:http://www.only2fire.com/archives/134.html
注:转载请注明出处,谢谢!^_^