• Linux学习笔记之设备驱动篇(3)_内核模块_实验篇


    零基础学Linux内核系列文章目录

    前置知识篇
    1. 进程
    2. 线程
    进程间通信篇
    1. IPC概述
    2. 信号
    3. 消息传递
    4. 同步
    5. 共享内存区
    编译相关篇
    1. GCC编译
    2. 静态链接与动态链接
    3. makefile入门基础
    设备驱动篇
    1. 设备驱动概述
    2. 内核模块_理论篇
    3. 内核模块_实验篇


    一、前言

    本节根据上节介绍的内核设备原理,做几个相关的实验。


    二、前置条件


    三、本文参考资料

    《 [野火]i.MX Linux开发实战指南》
    百度


    四、正文部分

    4.1 hellomodule实验

    4.1.1 实验代码

     #include 
     #include 
     #include 
    
     static int __init hello_init(void)
     {
    	/* 在内核模块运行的过程中,不能依赖于C库函数, 因此用不了printf函数,需要使用单独的打印输出函数printk */
    	printk(KERN_EMERG "[ KERN_EMERG ]  Hello  Module Init\n");
    	printk( "[ default ]  Hello  Module Init\n");
    	return 0;
     }
    
     static void __exit hello_exit(void)
     {
    	printk("[ default ]   Hello  Module Exit\n");
     }
    
     module_init(hello_init);
     module_exit(hello_exit);
    
     MODULE_LICENSE("GPL2");
     MODULE_AUTHOR("embedfire ");
     MODULE_DESCRIPTION("hello module");
     MODULE_ALIAS("test_module");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    4.1.2 代码框架分析

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

    • 模块加载函数(必须):
      当通过insmod或modprobe命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。
    • 模块卸载函数(必须):
      当执行rmmod命令卸载模块时,模块卸载函数就会自动被内核执行,完成相关清理工作。
    • 模块许可证声明(必须):
      许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。
    • 模块参数:
      模块参数是模块被加载时,可以传值给模块中的参数。
    • 模块导出符号:
      模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。
    • 模块的其他相关信息:
      可以声明模块作者等信息。

    完成模块初始化函数之后,还需要调用宏module_init来告诉内核,使用hello_init函数来进行初始化。
    模块卸载函数用宏module_exit在内核注册该模块的卸载函数。

    最后,必须声明该模块使用遵循的许可证,这里我们设置为GPL2协议。

    4.1.3 头文件

    前面我们已经接触过了Linux的应用编程,了解到Linux的头文件都存放在/usr/include中。
    编写内核模块所需要的头文件,并不在上述说到的目录,而是 在Linux内核源码中的include文件夹

    init.h头文件(位于内核源码 /include/linux/init.h)

    /* These are for everybody (although not all archs will actually discard it in modules) */
    #define __init __section(.init.text) __cold notrace
    #define __initdata __section(.init.data)
    #define __initconst __constsection(.init.rodata)
    #define __exitdata __section(.exit.data)
    #define __exit_call __used __section(.exitcall.exit)
    
    #define module_init(x) __initcall(x);
    
    #define module_exit(x) __exitcall(x);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    init.h头文件主要包含了内核模块的加载、卸载函数的声明,还有一些宏定义,
    因此,只要我们涉及内核模块的编程,就需要加上该头文件。

    module.h头文件(位于内核源码/include/linux/module.h)

    /* Generic info of form tag = "info" */
    #define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
    
    #define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)
    #define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
    
    #define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    以上代码中,列举了module.h文件中的部分宏定义,这部分宏定义,有的是可有可无的,
    但是MODULE_LICENSE这个是指定该内核模块的许可证,是必须要有的。

    4.1.4 许可证

    Linux是一款免费的操作系统,采用了GPL协议,允许用户可以任意修改其源代码。
    GPL协议的主要内容是软件产品中即使使用了某个GPL协议产品提供的库, 衍生出一个新产品,该软件产品都必须采用GPL协议,即必须是开源和免费使用的,可见GPL协议具有传染性。
    因此,我们可以在Linux使用各种各样的免费软件。

    许可证
    #define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
    
    • 1
    • 2

    内核模块许可证有 “GPL”,“GPL v2”,“GPL and additional rights”,“Dual SD/GPL”,“Dual MPL/GPL”,“Proprietary”。

    4.1.5 模块加载/卸载函数

    • module_init
      func_init函数在内核模块中也是做与初始化相关的工作。

      内核模块加载函数(位于内核源码/include/linux/module.h)
      static int __init func_init(void)
      {
      }
      module_init(func_init);
      
      返回值:
      	0: 表示模块初始化成功,并会在/sys/module下新建一个以模块名为名的目录,如下图中的红框处
      	非0: 表示模块初始化失败
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

      在这里插入图片描述
      内核模块的代码,实际上是内核代码的一部分,
      假如内核模块定义的函数和内核源代码中的某个函数重复了,编译器就会报错,导致编译失败,
      因此我们给内核模块的代码加上static修饰符的话, 那么就可以避免这种错误。

      __init、__initdata宏定义(位于内核源码/include/linux/init.h)
      #define __init      __section(.init.text) __cold  __latent_entropy __noinitretpoline
      #define __initdata  __section(.init.data)
      
      • 1
      • 2
      • 3

      以上代码 __init、__initdata宏定义中的__init用于修饰函数,__initdata用于修饰变量。
      带有__init的修饰符,表示将该函数放到可执行文件的 __init节区中, 该节区的内容只能用于模块的初始化阶段,初始化阶段执行完毕之后,这部分的内容就会被释放掉

      module_init宏定义
      #define module_init(x) __initcall(x);
      
      • 1
      • 2

      宏定义module_init用于通知内核初始化模块的时候,要使用哪个函数进行初始化。
      它会将函数地址加入到相应的节区section中, 这样的话,开机的时候就可以自动加载模块了。

    • module_exit
      与内核加载函数相反,内核模块卸载函数func_exit主要是用于释放初始化阶段分配的内存,分配的设备号等,是初始化过程的逆过程。

      内核模块卸载函数
      static void __exit func_exit(void)
      {
      }
      module_exit(func_exit);
      
      • 1
      • 2
      • 3
      • 4
      • 5

      与函数func_init区别在于,该函数的返回值是void类型,且修饰符也不一样,
      这里使用的__exit,表示将该函数放在可执行文件的 __exit节区当执行完模块卸载阶段之后,就会自动释放该区域的空间。

      __exit、__exitdata宏定义
      #define __exit __section(.exit.text) __exitused __cold notrace
      #define __exitdata __section(.exit.data)
      
      • 1
      • 2
      • 3

      类比于模块加载函数,__exit用于修饰函数,__exitdata用于修饰变量。
      宏定义module_exit用于告诉内核,当卸载模块时,需要调用哪个函数。

    4.1.6 printk函数

    • printf:glibc实现的打印函数,工作于用户空间

    • printk:内核模块无法使用glibc库函数,内核自身实现的一个类printf函数,但是需要指定打印等级。

      #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>” 调试信息
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

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

      打印内核所有打印信息:

      dmesg
      
      • 1

      注意内核log缓冲区大小有限制,缓冲区数据可能被覆盖掉。

    4.1.7 编译(makefile说明)

    对于内核模块而言,它是属于内核的一段代码,只不过它并不在内核源码中。
    为此,我们在编译时需要到内核源码目录下进行编译(借助export
    编译内核模块使用的Makefile文件,和我们前面编译C代码使用的Makefile大致相同,
    这得益于编译Linux内核所采用的Kbuild系统
    因此在编译内核模块时,我们也需要指定环境变量(体系架构)ARCH和编译工具链CROSS_COMPILE的值
    –> 实际上是使用内核的makefile进行编译,所以需要将条件导出

    # 内核源码的目录
    KERNEL_DIR=../../ebf_linux_kernel/build_image/build
    
    # 指定工具链ARCH=arm
    CROSS_COMPILE=arm-linux-gnueabihf-
    # 导出环境变量给子makefile使用
    export  ARCH  CROSS_COMPILE
    
    # 需要编译成模块的目标文件名(定义要生成的模块)
    obj-m := hellomodule.o
    
    # 执行linux顶层makefile的伪目标modules
    all:
        $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
    # -C表让make工具跳转到Linux内核目录下读取顶层makefile
    # ’M=$(CURDIR)’表明返回到当前目录,读取并执行当前目录的Makefile,开始编译内核模块
    
    .PHONE:clean copy
    
    clean:
        $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
    
    copy:
        sudo  cp  *.ko  /home/embedfire/workdir
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    4.1.8 运行(如何使用内核模块)

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

    • insmod
      如果要将一个模块加载到内核中,insmod是最简单的办法,
      insmod+模块完整路径就能达到目的,前提是你的模块不依赖其他模块,还要注意需要sudo权限。

      如果你不确定是否使用到其他模块的符号,你也可以尝试modprobe,后面会有它的详细用法。

      通过insmod命令加载hellomodule.ko内存模块加载该内存模块的时候,
      该内存模块会 自动执行module_init()函数,进行初始化操作,该函数打印了‘hello module init’。
      再次查看已载入系统的内核模块,我们就会在列表中发现hellomodule.ko的身影。
      在这里插入图片描述

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

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

    • rmmod
      rmod工具仅仅是将内核中运行的模块删除,只需要传给它路径就能实现。
      rmmod命令卸载某个内存模块时,内存模块会 自动执行 *_exit()函数 ,进行清理操作,
      我们的hellomodule中的*_exit()函数打印了一行内容,但是控制台并没有显示,可以使用dmesg查看, 之所以没有显示是与printk的打印等级有关,前面有关于printk函数有详细讲解。 rmmod不会卸载一个模块所依赖的模块,需要依次卸载,当然是用modprobe -r 可以一键卸载

    • modinfo
      modinfo用来显示我们在内核模块中定义的几个宏。
      我们可以通过modinfo来查看hellomodule,我们从打印的输出信息中,可以了解到,该模块遵循的是GPL协议, 该模块的作者是embedfire,该模块的vermagic等等。

      而这些信息在模块代码中由相关内核模块信息声明函数声明
      在这里插入图片描述

    4.1.9 自动加载

    1. 所有内核模块统一放到"/lib/modules/内核版本"目录下

      查看内核版本
      	uname -r
      
      cp *.ko /lib/modules/内核版本
      
      • 1
      • 2
      • 3
      • 4

      在这里插入图片描述

    2. 建立模块依赖关系:

      depmod -a 
      
      • 1
    3. 查看模块依赖关系

      cat /lib/modules/内核版本/modules.dep
      
      • 1
    4. 加载模块及其依赖模块

      modprobe xxx
      
      • 1

      最后在/etc/modules加上我们自己的模块,
      注意在该配置文件中,模块不写成.ko形式代表该模块与内核紧耦合,有些是系统必须要跟内核紧耦合,

      比如mm子系统, 一般写成.ko形式比较好,如果出现错误不会导致内核出现panic错误,如果集成到内核,出错了就会出现panic。
      在这里插入图片描述
      在这里插入图片描述

    5. 卸载模块及其依赖模块

      modprobe -r xxx (不用加.ko的后缀)
      
      • 1

      在这里插入图片描述

     

    4.2 传参实验

    4.2.1 参数传递函数

    内核模块作为一个可拓展的动态模块,为Linux内核提供了灵活性,
    但是有时我们需要根据不同的应用场景给内核传递不同的参数
    例如在程序中开启调试模式、设置详细输出模式以及制定与具体模块相关的选项,都可以通过参数的形式来改变模块的行为。

    Linux内核提供一个宏来实现模块的参数传递

    module_param函数 (内核源码/include/linux/moduleparam.h)
     #define module_param(name, type, perm)              \
         module_param_named(name, name, type, perm)
    
     #define module_param_array(name, type, nump, perm)      \
         module_param_array_named(name, name, type, nump, perm)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    以上代码中的module_param函数需要传入三个参数:

    • type:
      参数的类型,目前内核支持的参数类型有byte,short,ushort,int,uint,long,ulong,charp,bool,invbool。
      其中charp表示的是字符指针,bool是布尔类型,其值只能为0或者是1;
      invbool是反布尔类型,其值也是只能取0或者是1,但是true值表示0,false表示1。
      变量是char类型时,传参只能是byte,char * 时只能是charp。

    • perm: 表示的是该文件的权限,具体参数值见下表。
      在这里插入图片描述

    /sys/moudule/模块名/parameters
    不设置权限(即设为0),则不会生成该文件
    Char --> byte char * --> charp

    设置为写权限,则可以通过文件的方式对其进行访问并读写,完成修改参数
    上述文件权限唯独没有关于可执行权限的设置,请注意,该文件不允许它具有可执行权限
    如果强行给该参数赋予表示可执行权限的参数值S_IXUGO, 那么最终生成的内核模块在加载时会提示错误。
    char取值-127-128

    4.2.2 示例代码

    static int itype=0;
    module_param(itype,int,0);
    
    static bool btype=0;
    module_param(btype,bool,0644);
    
    static char ctype=0;
    module_param(ctype,byte,0);
    
    static char  *stype=0;
    module_param(stype,charp,0644);
    
    static int __init param_init(void)
    {
    	printk(KERN_ALERT "param init!\n");
    	printk(KERN_ALERT "itype=%d\n",itype);
    	printk(KERN_ALERT "btype=%d\n",btype);
    	printk(KERN_ALERT "ctype=%d\n",ctype);
    	printk(KERN_ALERT "stype=%s\n",stype);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    四个模块参数,会在 ‘/sys/module/模块名/parameters’ 下会存在以模块参数为名的文件。
    由于itype和ctype的权限是0,所以我们没有权限查看该参数。

    在这里插入图片描述
    在这里插入图片描述

     

    4.3 符号共享实验

    符号指的就是在内核模块中导出函数和变量,在加载模块时被记录在公共内核符号表中,以供其他模块调用。

    这个机制,允许我们使用分层的思想解决一些复杂的模块设计。

    我们在编写一个驱动的时候,可以把驱动按照功能分成几个内核模块,借助符号共享去实现模块与模块之间的接口调用,变量共享。

    #define EXPORT_SYMBOL(sym) \\
    __EXPORT_SYMBOL(sym, "")
    
    EXPORT_SYMBOL宏用于向内核导出符号,这样的话,其他模块也可以使用我们导出的符号了。
    
    parametermodule.c
    	...省略代码...
    	static int itype=0;
    	module_param(itype,int,0);
    	
    	EXPORT_SYMBOL(itype);
    	
    	int my_add(int a, int b)
    	{
    		return a+b;
    	}
    	
    	EXPORT_SYMBOL(my_add);
    	
    	int my_sub(int a, int b)
    	{
    		return a-b;
    	}
    	
    	EXPORT_SYMBOL(my_sub);
    	...省略代码...
    
    calculation.h
    	#ifndef __CALCULATION_H__
    	#define __CALCULATION_H__
    	
    	extern int itype;
    	
    	int my_add(int a, int b);
    	int my_sub(int a, int b);
    	
    	#endif
    	
    calculation.c
    	...省略代码...
    	#include "calculation.h"
    	
    	...省略代码...
    	static int __init calculation_init(void)
    	{
    		printk(KERN_ALERT "calculation  init!\n");
    		printk(KERN_ALERT "itype+1 = %d, itype-1 = %d\n", my_add(itype,1), my_sub(itype,1));
    		return 0;
    	}
    ...省略代码...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    calculation.c中使用extern关键字声明的参数itype,调用my_add()、my_sub()函数进行计算。

     


    五、总结

  • 相关阅读:
    支持C#的开源免费、新手友好的数据结构与算法入门教程 - Hello算法
    网站被大量cc攻击导致打不开怎么解决
    数据库容灾 | MySQL MGR与阿里云PolarDB-X Paxos的深度对比
    Vue18 v-for指令 展示列表数据
    如何设计科研问卷?
    基于MATLAB的一级倒立摆控制仿真,带GUI界面操作显示倒立摆动画,控制器控制输出
    密码学与加密算法详解
    文件服务之FTP
    c++函数模板不能作模板参数
    软件项目管理 7.4.2.进度计划编排-关键路径法
  • 原文地址:https://blog.csdn.net/qq_38211182/article/details/126443745