1、 linux 内核模块简介

内核整体结构非常庞大,其包含的组件也非常多。我们怎么把需要的部分都包含在内核中呢?

一种办法是把所有的需要的功能都编译到内核中。这会导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或删除功能,不得不重新编译内核,工作效率会非常的低,同时如果编译的模块不是很完善,很有可能会造成内核崩溃。 

提供了另一种机制来解决这个问题,这种集中被称为模块,可以实现编译出的内核本身并不含有所有功能,而在这些功能需要被使用的时候,其对应的代码可以被动态的加载到内核中。

2、 模块特点:

)模块本身并不被编译入内核,从而控制了内核的大小。

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

3、 最简单的模块

1) 下面是一个最简单的内核模块例子:

表 1 “hello world” 2.6内核模块

#include <linux/init.h>         /* printk() */
#include <linux/module.h>     /* __init __exit */
static int  __init  hello_init(void)      
模块加载函数,通过insmod命令加载模块时,被自动执行*/
{
  printk(KERN_INFO " Hello World enter\n");
  return 0;
}
static void  __exit  hello_exit(void)     
模块卸载函数,当通过rmmod命令卸载时,会被自动执行*/
{
  printk(KERN_INFO " Hello World exit\n ");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_AUTHOR("dengwei");       /*模块作者,可选*/
MODULE_LICENSE("Dual BSD/GPL"); /*模块许可证明,描述内核模块的许可权限,必须*/
MODULE_DESCRIPTION("A simple Hello World Module"); /*模块说明,可选*/
MODULE_ALIAS("a simplest module");                  /*模块说明,可选*/

2) 以下是编译上述模块所需的编写的makefile

表 2 2.6内核模块编译makefile

obj-m :=hello.o           //目标文件
#module-objs := file1.o file.o      //当模块有多个文件组成时,添加本句
KDIR :=/usr/src/linux            //内核路径
PWD := $(shell pwd)            //模块源文件路径
all: 
$(MAKE)  -C  $(KDIR)  SUBDIRS=$(PWD)  modules
@rm -rf *.mod.*
@rm -rf .*.cmd
@rm -rf *.o
@rm -rf Module.*
clean:
rm -rf *.ko

最终会编译得到:hello.ko文件

使用insmod hello.ko将模块插入内核,然后使用dmesg 即可看到输出提示信息。

3) 模块操作函数

4、 linux内核模块的程序结构

1) 模块加载函数:

Linux内核模块一般以__init标示声明,典型的模块加载函数的形式如下:

static int __init myModule_init(void)
{
/* Module init code */
PRINTK("myModule_init\n");
return 0;
}
module_init(myModule_init);

模块加载函数必须以“module_init(函数名)”的形式被指定。它返回整形值,若初始化成功,应返回0,初始化失败返回负数。

在linix2.6 内核中,可以使用:

request_module(module_name);

或request_module(“char-marjor-%d-%d”);

来加载其它内核模块。

2)  模块卸载函数

典型的模块卸载函数形式如下:

static void __exit myModule_exit(void)
{
/* Module exit code */
PRINTK("myModule_exit\n");
return;
}
module_exit(myModule_exit);

模块卸载函数在模块卸载的时候执行,不返回任何值,需用”module_exit(函数名)”的形式被指定。

卸载模块完成与加载函数相反的功能:

若加载函数注册了XXX,则卸载函数应当注销XXX

若加载函数申请了内存空间,则卸载函数应当释放相应的内存空间

若加载函数申请了某些硬件资源(中断、DMA、I/0端口、I/O内存等),则卸载函数应当释放相应的硬件资源

若加载函数开启了硬件,则卸载函数应当关闭硬件。

其中__init 、__exit 为系统提供的两种宏,表示其所修饰的函数在调用完成后会自动回收内存。

3) 模块参数

我们可以利用module_param(参数名、参数类型、参数读写属性) 为模块定义一个参数,例如:

static char *string_test = “this is a test”;
static num_test = 1000;  
module_param (num_test,int,S_IRUGO);
module_param (steing_test,charp,S_ITUGO);

在装载模块时,用户可以给模块传递参数,形式为:”insmod 模块名 参数名=参数值”,如果不传递,则参数使用默认的参数值。

参数的类型可以是:byte,short,ushort,int,uint,long,ulong,charp,bool.

模块被加载后,在sys/module/下会出现以此模块命名的目录。

当读写权限为零时:表示此参数不存在sysfs文件系统下的文件节点

当读写权限不为零时:此模块的目录下会存在parameters目录,包含一系列以参数名命名的文件节点,这些文件爱节点的权限值就是传入module_param()的“参数读/写权限“,而该文件的内容为参数的值。

除此之外,模块也可以拥有参数数组,形式为:”module_param_array(数组名、数组类型、数组长、参数读写权限等)”,当不需要保存实际的输入的数组元素的个数时,可以设置“数组长“为0。

运行lsmod时,使用都好分隔输入的数组元素。

下面是一个实际的例子,来说明模块传参的过程。

/*==================================================================
    A simple kernel module: "hello world"
===================================================================*/
#include <linux/init.h>
#include <linux/module.h>
static char *string_test="this is a test";
static int num_test=1000;
static int __init hello_init(void)
{
  printk(KERN_INFO "Hello World enter\n");
  printk(KERN_INFO "the test string is %s\n",string_test);
  printk(KERN_INFO "the test num is %d\n",num_test);
  return 0;
}
static void __exit hello_exit(void)
{
  printk(KERN_INFO " Hello World exit\n ");
}
module_param(num_test,int,S_IRUGO);
module_param(string_test,charp,S_IRUGO);
module_init(hello_init);
module_exit(hello_exit);
MODULE_AUTHOR("dengwei");
MODULE_LICENSE("Dual BSD/GPL");
MODULE_DESCRIPTION("A simple Hello World Module");
MODULE_ALIAS("a simplest module");

当执行 insmod hello_param.ko 时,执行dmesg 查看内核输出信息:

Hello World enter
the test string is: this is a test
the test num is :1000

当执行 insmod  heello_param.ko num_test=2000 string_test=”edit by dengwei”,执行dmesg查看内核输出信息:

Hello World enter
the test string is: edit by dengwei
the test num is :2000

4) 导出符号

Linix 2.6 内核的“/proc/kallsyms“文件对应内核符号表,它记录了符号以及符号所在的内存地址,模块可以使用下列宏导到内核符号表中。

EXPORT_SYMBOL(符号名);       任意模块均可

EXPORT_SYMBOL_GPL(符号名);  只使用于包含GPL许可权的模块

导出的符号可以被其它模块使用,使用前声明一下即可。

下面给出一个简单的例子:将add sub符号导出到内核符号表中,这样其它的模块就可以利用其中的函数

/*=================================================================
    A simple kernel module to introduce export symbol
=================================================================*/
#include <linux/init.h>                                
#include <linux/module.h>                                
MODULE_LICENSE("Dual BSD/GPL");       
int add_integar(int a,int b)                                
{                                
return a+b;                             
} 
                               
int sub_integar(int a,int b)   
{                                
return a-b;                             
}                            
EXPORT_SYMBOL(add_integar);
EXPORT_SYMBOL(sub_integar);

执行 cat /proc/kallsyms | grep integar 即可找到以下信息,表示模块确实被加载到内核表中。

f88c9008 r __ksymtab_sub_integar        [export_symb]
f88c9020 r __kstrtab_sub_integar         [export_symb]
f88c9018 r __kcrctab_sub_integar         [export_symb]
f88c9010 r __ksymtab_add_integar        [export_symb]
f88c902c r __kstrtab_add_integar          [export_symb]
f88c901c r __kcrctab_add_integar         [export_symb]
f88c9000 T add_integar                [export_symb]
f88c9004 T sub_integar                [export_symb]
13db98c9 a __crc_sub_integar           [export_symb]
e1626dee a __crc_add_integar           [export_symb]


5) 模块声明与描述

在linux内核模块中,我们可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_TABLE、MODULE_ALIA,分别描述模块的作者、描述、版本、设备表号、别名等。

MODULE_AUTHOR("dengwei");
MODULE_LICENSE("Dual BSD/GPL");
MODULE_DESCRIPTION("A simple Hello World Module");
MODULE_ALIAS("a simplest module");


6) 模块的使用计数

Linux2.4内核中,模块使用MOD_INC_USE_COUNT、MOD_DEC_USE_COUNT宏来管理自己被使用的次数。

Linux2.6内核中,提供了模块计数管理接口 try_module_get(&module) 和 module_put(&module),从而取代linux2.4内核中的模块使用计数管理宏。

Int try_module_get( struct *module);

该函数用于增加模块使用次数,若返回值为0,表示调用失败,希望使用的模块没有被加载或正在被加载中。

Void module_put(struct * module);

用于减少模块使用次数。

这两个函数的引入与使用与linux2.6下设备模型密切相关。Linux2.6内核为不同类型的设备定义了struct module *owner 域,用来指向管理此设备的模块。当开始使用某个设备时,使用try_module_get 函数去增加管理此设备的ower模块的使用次数,不用时调用  module_put(&module) 函数减少使用次数。这样,当设备在使用时,管理设备的模块不能被卸载,只有当设备不再不使用时,相应的模块才可以卸载。