Linux内核与驱动学习记录-最简单的内核模块-Hello内核模块
 2021.05.09    |      Linux MMC子系统    |     AilsonJack    |     暂无评论    |     334 views
By: Ailson Jack
Date: 2021-05-09
个人博客: http://www.only2fire.com/
<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">/** &nbsp;*&nbsp;@file&nbsp;hello_module.c &nbsp;*&nbsp;@author&nbsp;Ailson&nbsp;Jack&nbsp;(jackailson@foxmail.com) &nbsp;*&nbsp;@brief &nbsp;*&nbsp;@version&nbsp;1.0 &nbsp;*&nbsp;@date&nbsp;2021-05-08 &nbsp;* &nbsp;*&nbsp;@copyright&nbsp;Copyright&nbsp;(c)&nbsp;2021 &nbsp;* &nbsp;*&nbsp;@note&nbsp;blog:www.only2fire.com &nbsp;* &nbsp;*/ #include&nbsp;&lt;linux/init.h&gt; #include&nbsp;&lt;linux/module.h&gt; #include&nbsp;&lt;linux/kernel.h&gt; /*&nbsp;内核模块加载函数&nbsp;*/ static&nbsp;int&nbsp;__init&nbsp;hello_module_init(void) { &nbsp;&nbsp;&nbsp;&nbsp;printk(KERN_EMERG&nbsp;&quot;[KERN_EMERG]&nbsp;Hello&nbsp;Module&nbsp;init!\r\n&quot;); &nbsp;&nbsp;&nbsp;&nbsp;printk(&quot;[default]&nbsp;Hello&nbsp;Module&nbsp;init!\r\n&quot;); &nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;0; } /*&nbsp;内核模块卸载函数&nbsp;*/ static&nbsp;void&nbsp;__exit&nbsp;hello_module_exit(void) { &nbsp;&nbsp;&nbsp;&nbsp;printk(KERN_EMERG&nbsp;&quot;[KERN_EMERG]&nbsp;Hello&nbsp;Module&nbsp;exit!\r\n&quot;); &nbsp;&nbsp;&nbsp;&nbsp;printk(&quot;[default]&nbsp;Hello&nbsp;Module&nbsp;exit!\r\n&quot;); } module_init(hello_module_init); module_exit(hello_module_exit); MODULE_LICENSE(&quot;GPL&nbsp;v2&quot;);&nbsp;//表示模块代码接受的软件许可协议 MODULE_AUTHOR(&quot;Ailson&nbsp;Jack&quot;);&nbsp;//描述模块的作者信息 MODULE_DESCRIPTION(&quot;hello&nbsp;module&quot;);&nbsp;//对模块的简单介绍 MODULE_ALIAS(&quot;test_module&quot;);&nbsp;//给模块设置一个别名</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 &lt;linux/module.h&gt;:包含内核模块信息声明的相关函数;<br/></p><p style="text-indent: 2em;">(2).#include &lt;linux/init.h&gt;:包含了 module_init() 和 module_exit() 函数的声明;<br/></p><p style="text-indent: 2em;">(3).#include &lt;linux/kernel.h&gt;: 包含内核提供的各种函数,如 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&nbsp;KERN_EMERG&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&lt;0&gt;&quot;&nbsp;//通常是系统崩溃前的信息 #define&nbsp;KERN_ALERT&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&lt;1&gt;&quot;&nbsp;//需要立即处理的消息 #define&nbsp;KERN_CRIT&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&lt;2&gt;&quot;&nbsp;//严重情况 #define&nbsp;KERN_ERR&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&lt;3&gt;&quot;&nbsp;//错误情况 #define&nbsp;KERN_WARNING&nbsp;&nbsp;&nbsp;&nbsp;&quot;&lt;4&gt;&quot;&nbsp;//有问题的情况 #define&nbsp;KERN_NOTICE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&lt;5&gt;&quot;&nbsp;//注意信息 #define&nbsp;KERN_INFO&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&lt;6&gt;&quot;&nbsp;//普通消息 #define&nbsp;KERN_DEBUG&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&lt;7&gt;&quot;&nbsp;//调试信息</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(&#39;/uploads/AilsonJack/2021.05.09/160419247858139.png&#39;)"/></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的内容输出到控制台,那么你可以将&quot;默认消息日志级别&quot;设置为小于4,可以设置为3(只需要数值小于控制台日志级别即可),执行的命令如下:<br/></p><pre class="brush:bash;toolbar:false PrismJs">sudo&nbsp;sh&nbsp;-c&nbsp;&quot;echo&nbsp;&#39;4&nbsp;3&nbsp;1&nbsp;7&#39;&nbsp;&gt;&nbsp;/proc/sys/kernel/printk&quot;</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: &quot;Open Sans&quot;, &quot;Clear Sans&quot;, &quot;Helvetica Neue&quot;, 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">#&nbsp;指向编译出来的&nbsp;linux&nbsp;内核具体路径 KERNEL_DIR&nbsp;=&nbsp;../kernel/ebf-buster-linux/build_image/build #&nbsp;定义变量,并且导出变量给子&nbsp;Makefile&nbsp;使用 ARCH&nbsp;=&nbsp;arm CROSS_COMPILE&nbsp;=&nbsp;arm-linux-gnueabihf- export&nbsp;ARCH&nbsp;CROSS_COMPILE #&nbsp;obj-m&nbsp;:=&nbsp;&lt;模块名&gt;.o:&nbsp;定义要生成的模块 obj-m&nbsp;:=&nbsp;hello_module.o #&nbsp;选项&nbsp;&quot;-C&quot;:让&nbsp;make&nbsp;工具跳转到&nbsp;linux&nbsp;内核目录下读取顶层&nbsp;Makefile #&nbsp;&quot;M=&quot;&nbsp;表示内核模块源码目录 #&nbsp;$(CURDIR):&nbsp;Makefile&nbsp;默认变量,值为当前目录所在路径 #&nbsp;make&nbsp;modules:&nbsp;执行&nbsp;Linux&nbsp;顶层&nbsp;Makefile&nbsp;的伪目标,它实现内核模块的源码读取并编译为.ko文件 all: &nbsp;&nbsp;&nbsp;&nbsp;$(MAKE)&nbsp;-C&nbsp;$(KERNEL_DIR)&nbsp;M=$(CURDIR)&nbsp;modules .PHONY:clean&nbsp;copy clean: &nbsp;&nbsp;&nbsp;&nbsp;$(MAKE)&nbsp;-C&nbsp;$(KERNEL_DIR)&nbsp;M=$(CURDIR)&nbsp;clean copy: &nbsp;&nbsp;&nbsp;&nbsp;cp&nbsp;*.ko&nbsp;/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/内核版本”目录下,内核版本使用 &#39;uname -r&#39;查询;其次使用 depmod 建立模块之间的依赖关系,命令’ depmod -a’;这个时候我们就可以在 modules.dep 中看到模块依赖关系。<br/></p><p style="text-indent: 2em;">最后在/etc/modules 加上我们自己的模块,注意在该配置文件中,模块不写成.ko 形式代表该模块与内核紧耦合,有些是系统必须要跟内核紧耦合,比如 mm 子系统,一般写成.ko 形式比较好,如果出现错误不会导致内核出现 panic 错误,如果集成到内核,出错了就会出现panic。</p>
欢迎关注博主的公众号呀,精彩内容随时掌握:
热情邀请仔细浏览下博客中的广告,万一有对自己有用或感兴趣的呢。◕ᴗ◕。。
如果这篇文章对你有帮助,记得点赞和关注博主就行了^_^,当然了能够赞赏博主,那就非常感谢啦!
注: 转载请注明出处,谢谢!^_^
暂无评论,要不要来个沙发
发表评论

 
Copyright © 2015~2023  说好一起走   保留所有权利   |  百度统计  蜀ICP备15004292号