Linux内核与驱动学习记录-最简单的内核模块-Hello内核模块
<p class="artical_littlestyle1">1.内核模块的概念</p><p style="text-indent: 2em;">因为Linux 操作系统采用了宏内核结构,宏内核的优点是执行效率非常高,但缺点也是十分明显的,一旦我们想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核。为了解决这一缺点,Linux 中引入了内核模块这一机制。</p><p style="text-indent: 2em;">内核模块就是实现了某个功能的一段内核代码,在内核运行过程,可以加载这部分代码到内核中,从而动态地增加了内核的功能。基于这种特性,我们进行设备驱动开发时,以内核模块的形式编写设备驱动,只需要编译相关的驱动代码即可,无需对整个内核进行编译。内核模块的引入不仅提高了系统的灵活性,对于开发人员来说更是提供了极大的方便。<br/></p><p style="text-indent: 2em;">内核模块定义:内核模块全称 Loadable Kernel Module(LKM),是一种在内核运行时加载一组目标代码来实现某个特定功能的机制。<br/></p><p style="text-indent: 2em;">内核模块特点:</p><p style="text-indent: 2em;">(1).模块本身不被编译入内核映像,这控制了内核的大小;</p><p style="text-indent: 2em;">(2).模块一旦被加载,它就和内核中的其它部分完全一样。<br/></p><p style="text-indent: 2em;">我们编写的内核模块,经过编译,最终形成以.ko为后缀的文件。ko 文件在数据组织形式上是 ELF(Excutable And Linking Format) 格式,是一种普通的可重定位目标文件。</p><p class="artical_littlestyle2">2.编写Hello内核模块</p><p style="text-indent: 2em;">对于程序入门学习来说,Hello World程序是经典的例子,这里我们也实现一个简单的Hello内核模块用于了解内核模块编程的基本框架。<br/></p><p style="text-indent: 2em;">hello_module.c文件的内容如下所示:</p><pre class="brush:cpp;toolbar:false PrismJs">/**
* @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"); //给模块设置一个别名</pre><p style="text-indent: 2em;"><span style="color: rgb(0, 112, 192);"><strong>2.1.Hello内核模块代码框架分析</strong></span><br/></p><p style="text-indent: 2em;">Linux 内核模块的代码框架通常由下面几个部分组成:<br/></p><p style="text-indent: 2em;">(1).模块加载函数 (必须):当通过 insmod 或 modprobe 命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。<br/></p><p style="text-indent: 2em;">(2).模块卸载函数 (必须):当执行 rmmod 命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。<br/></p><p style="text-indent: 2em;">(3).模块许可证声明 (必须):许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。<br/></p><p style="text-indent: 2em;">(4).模块参数:模块参数是模块被加载时,可以传值给模块中的参数。<br/></p><p style="text-indent: 2em;">(5).模块导出符号:模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。<br/></p><p style="text-indent: 2em;">(6).模块的其他相关信息:可以声明模块作者等信息。</p><p style="text-indent: 2em;"><strong><span style="color: rgb(0, 112, 192);">2.2.内核模块头文件</span></strong><br/></p><p style="text-indent: 2em;">Hello内核模块中,使用3个头文件,下面说说这3个头文件具体提供的信息:<br/></p><p style="text-indent: 2em;">(1).#include <linux/module.h>:包含内核模块信息声明的相关函数;<br/></p><p style="text-indent: 2em;">(2).#include <linux/init.h>:包含了 module_init() 和 module_exit() 函数的声明;<br/></p><p style="text-indent: 2em;">(3).#include <linux/kernel.h>: 包含内核提供的各种函数,如 printk。</p><p style="text-indent: 2em;"><span style="color: rgb(0, 112, 192);"><strong>2.3.内核模块加载/卸载函数</strong></span></p><p style="text-indent: 2em;">module_init():声明内核模块加载函数,加载内核模块的时候会调用声明的内核模块加载函数,模块加载成功,会在<span style="color: rgb(255, 0, 0);"><strong>/sys/module</strong></span>下新建一个以模块名为名的目录。<br/></p><p style="text-indent: 2em;">module_exit():声明内核模块卸载函数,卸载内核模块的时候会调用声明的内核模块卸载函数。<br/></p><p style="text-indent: 2em;">__init 用于修饰函数, __initdata 用于修饰变量。带有 __init 的修饰符,表示将该函数放到可执行文件的 __init 节区中,该节区的内容只能用于模块的初始化阶段,初始化阶段执行完毕之后,这部分的内容就会被释放掉,真可谓是“针尖也要削点铁”。<br/></p><p style="text-indent: 2em;">__exit 用于修饰函数,__exitdata 用于修饰变量。带有__exit的修饰符,表示将该函数放到可执行文件的__exit节区,当执行完模块卸载阶段之后,就会自动释放该区域的空间。<br/></p><p style="text-indent: 2em;">注意:hello_module_init()函数的返回值是int,hello_module_exit()的返回值是void,并且这两个函数都使用static进行修饰,表示函数只能在本文件进行调用,不能被其他文件调用。</p><p style="text-indent: 2em;"><span style="color: rgb(0, 112, 192);"><strong>2.4.内核打印函数-printk</strong></span></p><p style="text-indent: 2em;">printk函数的打印等级:</p><pre class="brush:cpp;toolbar:false PrismJs">#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>" //调试信息</pre><p style="text-indent: 2em;">printk函数可以指定打印等级,当不指定打印等级的时候,会使用默认的打印等级。<br/></p><p style="text-indent: 2em;">查看当前系统 printk 打印等级: cat /proc/sys/kernel/printk,从左到右依次对应控制台日志级别、默认消息日志级别、最小的控制台日志级别、默认控制台日志级别。</p><p style="text-align:center"><img src="/uploads/AilsonJack/2021.05.09/160419247858139.png" onclick="preview_image('/uploads/AilsonJack/2021.05.09/160419247858139.png')"/></p><p style="text-indent: 2em;">控制台日志级别:优先级高于该值得消息将被打印到到控制台;<br/></p><p style="text-indent: 2em;">默认消息日志级别:将用该优先级来打印没有指定优先级的消息;<br/></p><p style="text-indent: 2em;">最小的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级);<br/></p><p style="text-indent: 2em;">默认控制台日志级别:控制台日志级别的缺省值。</p><p style="text-indent: 2em;"><strong>以上的数值设置,数值越小,优先级越高。</strong></p><p style="text-indent: 2em;">假设你想让hello_module_init()或者hello_module_exit()函数中,没有指定打印等级的printk的内容输出到控制台,那么你可以将"默认消息日志级别"设置为小于4,可以设置为3(只需要数值小于控制台日志级别即可),执行的命令如下:<br/></p><pre class="brush:bash;toolbar:false PrismJs">sudo sh -c "echo '4 3 1 7' > /proc/sys/kernel/printk"</pre><p style="box-sizing: border-box; line-height: inherit; margin: 0.8em 0px; white-space: pre-wrap; position: relative; color: rgb(51, 51, 51); font-family: "Open Sans", "Clear Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-align: start; text-transform: none; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; text-indent: 2em;"><span class="md-plain md-expand" style="box-sizing: border-box;">然后执行加载或者卸载模块,就可以看到未指定打印等级的消息输出到控制台了。</span></p><p style="text-indent: 2em;"><strong><span class="md-plain md-expand" style="box-sizing: border-box;">查看内核所有打印信息: dmesg,注意内核 log 缓冲区大小有限制,缓冲区数据可能被覆盖掉。</span></strong></p><p class="artical_littlestyle3">3.内核模块的makefile</p><p style="text-indent: 2em;">对于内核模块而言,它是属于内核的一段代码,只不过它并不在内核源码中。为此,我们在编译时需要到内核源码目录下进行编译。编译内核模块使用的 Makefile 文件,和我们前面编译 C 代码使用的 Makefile 大致相同,这得益于编译 Linux 内核所采用的 Kbuild 系统,因此在编译内核模块时,我们也需要指定环境变量 ARCH 和CROSS_COMPILE 的值。<br/></p><p style="text-indent: 2em;">编译Hello内核模块使用的Makefile文件内容如下:</p><pre class="brush:bash;toolbar:false PrismJs"># 指向编译出来的 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</pre><p style="text-indent: 2em;">在内核模块的目录中,执行make命令编译内核模块,生成hello_module.ko文件,将hello_module.ko文件通过nfs或者scp拷贝到开发板,即可加载该内核模块。</p><p class="artical_littlestyle4">4.内核模块常用命令</p><p style="text-indent: 2em;"><span style="color: rgb(0, 112, 192);"><strong>4.1.lsmod</strong></span><br/></p><p style="text-indent: 2em;">lsmod 列出当前内核中的所有模块,格式化显示在终端,其原理就是将/proc/module 中的信息调整一下格式输出。 lsmod 输出列表有一列 Used by,它表明此模块正在被其他模块使用,显示了模块之间的依赖关系。<br/></p><p style="text-indent: 2em;"><span style="color: rgb(0, 112, 192);"><strong>4.2.insmod<br/></strong></span></p><p style="text-indent: 2em;"><span style="color: rgb(0, 0, 0);">如果要将一个模块加载到内核中, insmod 是最简单的办法, insmod+模块完整路径就能达到目的,前提是你的模块不依赖其他模块,还要注意需要 sudo 权限。如果你不确定是否使用到其他模块的符号,你也可以尝试modprobe,后面会有它的详细用法。</span><span style="color: rgb(0, 112, 192);"><strong><br/></strong></span></p><p style="text-indent: 2em;"><strong><span style="color: rgb(0, 112, 192);">4.3.rmmod<br/></span></strong></p><p style="text-indent: 2em;"><span style="color: rgb(0, 0, 0);">rmod 工具仅仅是将内核中运行的模块删除,只需要传给它路径就能实现。<br/></span></p><p style="text-indent: 2em;"><span style="color: rgb(0, 0, 0);">rmmod 不会卸载一个模块所依赖的模块,需要依次卸载,当然用 modprobe -r 可以一键卸载。</span><strong><span style="color: rgb(0, 112, 192);"><br/></span></strong></p><p style="text-indent: 2em;"><strong><span style="color: rgb(0, 112, 192);">4.4.modprobe</span></strong><br/></p><p style="text-indent: 2em;">modprobe 和 insmod 具备同样的功能,同样可以将模块加载到内核中,除此以外 modprobe 还能检查模块之间的依赖关系,并且按照顺序加载这些依赖,可以理解为按照顺序多次执行 insmod。<br/></p><p style="text-indent: 2em;"><span style="color: rgb(0, 112, 192);"><strong>4.5.depmod</strong></span><br/></p><p style="text-indent: 2em;">modprobe 是怎么知道一个给定模块所依赖的其他的模块呢?在这个过程中, depend 起到了决定性作用,当执行 modprobe 时,它会在模块的安装目录下搜索 module.dep 文件,这是 depmod 创建的模块依赖关系的文件。<br/></p><p style="text-indent: 2em;"><strong><span style="color: rgb(0, 112, 192);">4.6.modinfo</span></strong><br/></p><p style="text-indent: 2em;">modinfo 用来显示内核模块一些信息。比如:modinfo hello_module.ko<br/></p><p class="artical_littlestyle1">5.系统自动加载内核模块</p><p style="text-indent: 2em;">我们自己编写了一个模块,或者说怎样让它在板子开机自动加载呢?这里就需要用到上述的 depmod 和 modprobe 工具了。<br/></p><p style="text-indent: 2em;">首先需要将我们想要自动加载的模块统一放到”/lib/modules/内核版本”目录下,内核版本使用 'uname -r'查询;其次使用 depmod 建立模块之间的依赖关系,命令’ depmod -a’;这个时候我们就可以在 modules.dep 中看到模块依赖关系。<br/></p><p style="text-indent: 2em;">最后在/etc/modules 加上我们自己的模块,注意在该配置文件中,模块不写成.ko 形式代表该模块与内核紧耦合,有些是系统必须要跟内核紧耦合,比如 mm 子系统,一般写成.ko 形式比较好,如果出现错误不会导致内核出现 panic 错误,如果集成到内核,出错了就会出现panic。</p>
你可能也喜欢:
暂无评论,要不要来个沙发
发表评论
JLink V9掉固件修复(灯不亮) 3Zephyr笔记2 - 在STM32F429上运行HelloWorld 2计算NandFlash要传入的行地址和列地址 1Linux MMC子系统 - 6.eMMC 5.1工作模式-设备识别模式 0Linux MMC子系统 - 5.eMMC 5.1工作模式-引导模式 0Linux MMC子系统 - 4.eMMC 5.1常用命令说明(2) 0
标签云
Linux嵌入式实用技巧ARM内核学习问题集合CC++编程语言阅读笔记汇编Linux内核完全注释Windows驱动开发计算机基础ARM11ARMv7-ASTM32IDESublimeLinux内核学习eMMCMMC子系统Ubuntu操作系统OfficeVMWareAPUEgccRTOS中断漫游世界随笔感悟开发工具软件应用编程VsCodearmccarmclang编译器ZephyrSPIJLink网卡驱动安装各种芯片库函数NFSμCOS内核sambaFlashUnix命令与脚本输入法Linux内核设计与实现gitRIFFWAVJATGFTPar8161安装centos有线上网μCGUI字库工程建立右键菜单网络文件系统Firefox百度NTFS文件系统CodeBlocksCentOS数据结构算法PhotoShop51KeilQTUltraEditscanfglibc宏定义UIDGID优先级娱乐天地SourceInsight磁盘扇区总线I2CPDFBComparePythonI2SFPUMakefileSWDCPUARP软件推荐FileZilla