Linux驱动--(四)内核模块参数详解

Linux驱动-内核模块参数

一、概述

前面几篇文章中,我们简单说了一个简单的内核模块的编写。在那里我说过模块加载函数接收参数。那么如果我们想要通过传参对模块进行控制那不是很难办到了。别急,Linux提供了一种命令行的方式来传递参数信息,就是所谓的模块参数

模块参数:简单来说模块参数允许用户再加载模块时通过命令行指定参数值,在模块的加载过程中,加载程序会得到命令行参数,并转换成相应类型的值,然后复制给对应的变量,这个过程发生在调用模块初始化函数之前。

内核支持的参数类型主要有:bool、字符串指针、short、int、long、ushort、uint、ulong。这些类型又可以复合成对应的数组类型。

下面申明两个变量,用于将变量申明为模块参数。

module_param(name,type,perm);
module_param_array(name,type,nump,perm);

@name:变量的名字
@type:变量或数组元素的类型
@nump:数组元素个数的指针,可选
@perm:在sysfs文件系统中对应的文件的权限属性。可参考				       <linux/stat.h>,和普通文件的权限一样,但是当perm为0时,	       sysfs文件文件系统中将不会出现对应的文件。
//备注一下,sysfs文件系统可用于内核和应用程序间通信的一种方式

下面来举个例子,还是利用前面的代码:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

static int baudrate = 9600;
static int port[4] = {0,1,2,3};
static char *name = "vser";

module_param(baudrate,int,S_IRUGO);
module_param_array(port,int,NULL,S_IRUGO);
module_param(name,charp,S_IRUGO);

static int __init hello_init(void)
{
    int i;
    
    printk("init module:hello world!\n ");
    printk("baudrate:%d\n");
    for(i = 0;i < ARRAY_SIZE(port);i++)
    {
        printk("%d",port[i]);
    }
    printk("\n");
    printk("name: %s\n",name);
    
    return 0;
}

static void __exit hello_exit(void)
{
    printk("exit module:hello_exit!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Fan <[email protected]>");
MODULE_DESCRIPTION("A hello module");
MODULE_ALIAS("printf_hello");

​ 编译完成后,加载模块。如果不指定模块参数的值,那么就会打印函数中已 经定义的值。如果按照如下指定:

# depmod
# modprobe vser baudrate=115200 port=1,2,3,4 name="hello"
# dmesg

​ 我们会发现内核会按照我们指定的参数进行打印,而原来的参数值会被覆盖掉。

​ 同样的我们可以去/sys/mododule/hello/ 目录下面去看看我们给参数。

二、内核模块的依赖关系

  • 在前面的代码中,我们使用了printk函数。我们知道他是内核函数,可是我们在只build模块的情况下,原来的代码竟然build过了。这是为什么呢?

    原来,我们刚才的代码并没有仅仅是进行了编译并没有进行链接。编译出来的hello.ko文件是一个普通的ELF目标文件,使用file命令和nm命令可以得到该模块的相关的细节信息。

    # file hello.ko
    # nm hello.ko
    

    此时呢,printk对整个文件而言是一个未决符号,因为他放在其他文件中(内核源码:/kernel/printk/printk.c)。所以我们编译时并不知道这个函数的地址,那么我们该怎么解决呢?其实这里Linux使用了一个很巧妙的类似于动态链接的方式。下面来说明一下:

    首先介绍一下EXPORT_SYMBOL的宏将printk导出,其目的是为动态加载的模块提供地址信息。(当然啦,此处我们也可以使用另一个宏:EXPORT_SYMBOL_GPL来进行导出)

    我们可以在Linux的源码该函数的实现代码中找到这样一行代码:

    EXPORT_SYMBOL(printk);
    

    其目的是为动态加载的模块提供printk的地址信息。

    工作原理大概是这样的:利用EXPORT_SYMBOL宏生成一个特定的结构并放到ELF文件的一个特定的段中,在内核的启动过程中,会将符号确切地址填充到这个结构的特定成员中。模块加载时,加载程序将去处理未决符号,在特殊段汇总搜索符号的名字,如果找到,则将获得的地址填充在被加载模块的相应段中,这样符号的地址就可以确定了。利用这种方式处理未决符号,就相当于把连接的过程推后进行了动态链接。和普通的应用程序使用共享库函数的道理是类似的。内核中有大量的符号导出,为模块提供丰富的基础设施。

    同样的,推广一下,如果一个模块需要提供全局变量或函数给到另外的模块使用,那么就需要将这些符号导出。

    这样细心的小伙伴就会发现了,两个模块之间会存在依赖关系。

    为了方便我们这里命名两个不同的模块:模块A、模块B。

    模块A提供给了模块B一些全局变量和全局函数。

    1. 如果我们在使用insmod命令加载模块B时,就必须先加载模块A。因为模块B使用了模块A导出的符号。如果没有加载模块A而先加载模块B,那么加载时就会失败,因为加载过程中因为处理那些未决符号而失败。

      这里我们要说明一下,上一篇博客提到的模块加载指令modprobe命令优于insmod的地方了。modprobe可以自动加载被依赖的模块,而这又归功于depmod命令将会生成模块的依赖信息保存在/lib/modules/3.14.0-32-gener.c/modules.dep文件中。3.14.0-32-gener.c为内核源码版本。

    2. 因为两个模块存在依赖关系,如果分别编译连个模块,将会出现警告,并且几遍加载顺序正确,加载也不会成功。

      这是因为,在编译模块时,模块B在内核符号表中找不到由模块A提供的一些函数和变量,而这些符号也不知道模块A的存在。解决的办法有两个:1.将两个模块一起编译。2.将模块A放到内核源码中,现在内核源码下编译完所有的模块再编译模块B。

    3. 卸载模块时,要先卸载模块模块B,在卸载模块A。否则,因为模块A会被模块B使用而无法卸载。

      内核将会创建模块依赖关系的链表,只有当这个模块的依赖链表为空时,模块才能被卸载。

三、总结

  • Linux内核是一个开源的项目,世界上有很多优秀的开发者在对齐进行着维护。为了与时俱进,Linux内核也在不断的更新换代。而这对于我们这些内核开发者而言,影响是巨大。因为每一次更新换代,Linux就有可能将一些以前的接口等进行修改,或者直接被剔除内核。那么如果改动了,我们之前编写的内核模块的调用的接口部分,那么对我们的影响是巨大的。内核模块.ko文件中会记录内核源码信息、系统结构想你想、函数接口信息等,在开启版本控制选项的内核中加载一个模块时,啮合将核对这些信息,如果不一致就会拒绝加载。所以呢,作为一个内核开发者,我们应该时刻关注Linux版本的更新详情。当然啦,如果你一直用同一个版本进行开发,而不到算进行跟换内核版本的话也是没有影响的。

  • 下面总结一下内核莫夸与普通应用程序之间的区别:

    1. 内核模块时操作系统的一部分,运行在内核空间;而应用程序运行在用户空间。
    2. 内核模块中的函数是被动的调用,比如模块的初始化函数和清除函数分别在模块加载和卸载时调用,而应用程序则是顺序执行,然后通常进入一个循环反复调用某些函数。
    3. 内核模块处于C函数库之下,自然不能调用C库函数,当然啦,内核源码中会有相似的实现,例如printk函数;而应用程序却可以随意调用C库函数。
    4. 内核模块要做一些清除性的工作,比如在一个操作失败后或者在诶和的清除函数中;而应用程序的有些工作通常可以不用做,比如在程序退出前关闭打开的文件。因为操作系统(内核)会帮它去做。
    5. 内核模块如果产生了非法访问将会导致整个系统的崩溃(如野指针),所以在进行内核块编程的时候,一定要注意指针的使用;而应用程序访问了非法内存只会影响自己,因为操作系统(内核)会帮它去处理。
    6. 内核模块中的并发更多,比如中断、多处理器;而应用程序中一般只考虑多进程或多线程。
    7. 整个内核的空间调用链上只有4kb或8kb的栈,相对于应用程序来说非常的小。如果需要更大内存空间通常需要动态分配。
    8. 虽然printk和printf的行为非常相似,但是通常printk不支持浮点数,例如要打印一个浮点变量。编译时会出现警告,并且模块也不会加载成功。内核开发中应该避免使用浮点数。

    好啦,说了这么多。下一步分开始字符设备的驱动了。

发布了49 篇原创文章 · 获赞 15 · 访问量 9256

猜你喜欢

转载自blog.csdn.net/wit_732/article/details/102907944