1.内核模块的概念

因为Linux 操作系统采用了宏内核结构,宏内核的优点是执行效率非常高,但缺点也是十分明显的,一旦我们想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核。为了解决这一缺点,Linux 中引入了内核模块这一机制。

内核模块就是实现了某个功能的一段内核代码,在内核运行过程,可以加载这部分代码到内核中,从而动态地增加了内核的功能。基于这种特性,我们进行设备驱动开发时,以内核模块的形式编写设备驱动,只需要编译相关的驱动代码即可,无需对整个内核进行编译。内核模块的引入不仅提高了系统的灵活性,对于开发人员来说更是提供了极大的方便。

内核模块定义:内核模块全称 Loadable Kernel Module(LKM),是一种在内核运行时加载一组目标代码来实现某个特定功能的机制。

内核模块特点:

模块本身不被编译入内核映像,这控制了内核的大小;

模块一旦被加载,它就和内核中的其它部分完全一样。

我们编写的内核模块,经过编译,最终形成以.ko为后缀的文件。ko 文件在数据组织形式上是 ELF(Excutable And Linking Format) 格式,是一种普通的可重定位目标文件。

2.编写Hello内核模块

对于程序入门学习来说,Hello World程序是经典的例子,这里我们也实现一个简单的Hello内核模块用于了解内核模块编程的基本框架。

hello_module.c文件的内容如下所示:

  1. /**
  2. * @file hello_module.c
  3. * @author Ailson Jack (jackailson@foxmail.com)
  4. * @brief
  5. * @version 1.0
  6. * @date 2021-05-08
  7. *
  8. * @copyright Copyright (c) 2021
  9. *
  10. * @note blog:www.only2fire.com
  11. *
  12. */#include<linux/init.h>#include<linux/module.h>#include<linux/kernel.h>/* 内核模块加载函数 */staticint__inithello_module_init(void){
  13.    printk(KERN_EMERG"[KERN_EMERG] Hello Module init!\r\n");
  14.    printk("[default] Hello Module init!\r\n");return0;
  15. }/* 内核模块卸载函数 */staticvoid__exithello_module_exit(void){
  16.    printk(KERN_EMERG"[KERN_EMERG] Hello Module exit!\r\n");
  17.    printk("[default] Hello Module exit!\r\n");
  18. }

  19. module_init(hello_module_init);
  20. module_exit(hello_module_exit);



MODULE_LICENSE("GPL v2");//表示模块代码接受的软件许可协议MODULE_AUTHOR("Ailson Jack");//描述模块的作者信息MODULE_DESCRIPTION("hello module");//对模块的简单介绍MODULE_ALIAS("test_module");//给模块设置一个别名

2.1.Hello内核模块代码框架分析

Linux 内核模块的代码框架通常由下面几个部分组成:

模块加载函数 (必须):当通过 insmod 或 modprobe 命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。

模块卸载函数 (必须):当执行 rmmod 命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。

模块许可证声明 (必须):许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。

模块参数:模块参数是模块被加载时,可以传值给模块中的参数。

模块导出符号:模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。

模块的其他相关信息:可以声明模块作者等信息。

2.2.内核模块头文件

Hello内核模块中,使用3个头文件,下面说说这3个头文件具体提供的信息:

#include <linux/module.h>:包含内核模块信息声明的相关函数;

#include <linux/init.h>:包含了 module_init() 和 module_exit() 函数的声明;

#include <linux/kernel.h>: 包含内核提供的各种函数,如 printk。

2.3.内核模块加载/卸载函数

module_init():声明内核模块加载函数,加载内核模块的时候会调用声明的内核模块加载函数,模块加载成功,会在/sys/module下新建一个以模块名为名的目录。

module_exit():声明内核模块卸载函数,卸载内核模块的时候会调用声明的内核模块卸载函数。

__init 用于修饰函数, __initdata 用于修饰变量。带有 __init 的修饰符,表示将该函数放到可执行文件的 __init 节区中,该节区的内容只能用于模块的初始化阶段,初始化阶段执行完毕之后,这部分的内容就会被释放掉,真可谓是“针尖也要削点铁”。

__exit 用于修饰函数,__exitdata 用于修饰变量。带有__exit的修饰符,表示将该函数放到可执行文件的__exit节区,当执行完模块卸载阶段之后,就会自动释放该区域的空间。

注意:hello_module_init()函数的返回值是int,hello_module_exit()的返回值是void,并且这两个函数都使用static进行修饰,表示函数只能在本文件进行调用,不能被其他文件调用。

2.4.内核打印函数-printk

printk函数的打印等级:

#defineKERN_EMERG"<0>"//通常是系统崩溃前的信息#defineKERN_ALERT"<1>"//需要立即处理的消息#defineKERN_CRIT"<2>"//严重情况#defineKERN_ERR"<3>"//错误情况#defineKERN_WARNING"<4>"//有问题的情况#defineKERN_NOTICE"<5>"//注意信息#defineKERN_INFO"<6>"//普通消息#defineKERN_DEBUG"<7>"//调试信息

printk函数可以指定打印等级,当不指定打印等级的时候,会使用默认的打印等级。

查看当前系统 printk 打印等级: cat /proc/sys/kernel/printk,从左到右依次对应控制台日志级别、默认消息日志级别、最小的控制台日志级别、默认控制台日志级别。

forum.jpg


控制台日志级别:优先级高于该值的消息将被打印到控制台;

默认消息日志级别:将用该优先级来打印没有指定优先级的消息;

最小的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级);

默认控制台日志级别:控制台日志级别的缺省值。

以上的数值设置,数值越小,优先级越高。

假设你想让hello_module_init()或者hello_module_exit()函数中,没有指定打印等级的printk的内容输出到控制台,那么你可以将"默认消息日志级别"设置为小于4,可以设置为3(只需要数值小于控制台日志级别即可),执行的命令如下:

sudosh -c"echo '4 3 1 7' > /proc/sys/kernel/printk"

然后执行加载或者卸载模块,就可以看到未指定打印等级的消息输出到控制台了。

查看内核所有打印信息: dmesg,注意内核 log 缓冲区大小有限制,缓冲区数据可能被覆盖掉。

3.内核模块的makefile

对于内核模块而言,它是属于内核的一段代码,只不过它并不在内核源码中。为此,我们在编译时需要到内核源码目录下进行编译。编译内核模块使用的 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-exportARCH 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 copyclean:$(MAKE)-C$(KERNEL_DIR)M=$(CURDIR)cleancopy:cp *.ko /home/ailsonjack/share/nfs/temp

在内核模块的目录中,执行make命令编译内核模块,生成hello_module.ko文件,将hello_module.ko文件通过nfs或者scp拷贝到开发板,即可加载该内核模块。

4.内核模块常用命令

4.1.lsmod

lsmod 列出当前内核中的所有模块,格式化显示在终端,其原理就是将/proc/module 中的信息调整一下格式输出。 lsmod 输出列表有一列 Used by,它表明此模块正在被其他模块使用,显示了模块之间的依赖关系。

4.2.insmod

如果要将一个模块加载到内核中, insmod 是最简单的办法, insmod+模块完整路径就能达到目的,前提是你的模块不依赖其他模块,还要注意需要 sudo 权限。如果你不确定是否使用到其他模块的符号,你也可以尝试modprobe,后面会有它的详细用法。

4.3.rmmod

rmod 工具仅仅是将内核中运行的模块删除,只需要传给它路径就能实现。

rmmod 不会卸载一个模块所依赖的模块,需要依次卸载,当然用 modprobe -r 可以一键卸载。

4.4.modprobe

modprobe 和 insmod 具备同样的功能,同样可以将模块加载到内核中,除此以外 modprobe 还能检查模块之间的依赖关系,并且按照顺序加载这些依赖,可以理解为按照顺序多次执行 insmod。

4.5.depmod

modprobe 是怎么知道一个给定模块所依赖的其他的模块呢?在这个过程中, depend 起到了决定性作用,当执行 modprobe 时,它会在模块的安装目录下搜索 module.dep 文件,这是 depmod 创建的模块依赖关系的文件。

4.6.modinfo

modinfo 用来显示内核模块一些信息。比如:modinfo hello_module.ko

5.系统自动加载内核模块

我们自己编写了一个模块,或者说怎样让它在板子开机自动加载呢?这里就需要用到上述的 depmod 和 modprobe 工具了。

首先需要将我们想要自动加载的模块统一放到”/lib/modules/内核版本”目录下,内核版本使用 'uname -r'查询;其次使用 depmod 建立模块之间的依赖关系,命令’ depmod -a’;这个时候我们就可以在 modules.dep 中看到模块依赖关系。

最后在/etc/modules 加上我们自己的模块,注意在该配置文件中,模块不写成.ko 形式代表该模块与内核紧耦合,有些是系统必须要跟内核紧耦合,比如 mm 子系统,一般写成.ko 形式比较好,如果出现错误不会导致内核出现 panic 错误,如果集成到内核,出错了就会出现panic。