1.linux内核的整体机构已经能够非常庞大,而其包含的组件也非常多,我们怎么把需要的部分都包含的内核中呢?
一种方法就是把所有需要的功能都编译到linux kernel,这会导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或者删除功能,将不得不重新编译内核。
而linux提供一种机制,这种机制叫做模块(module).模块具有这样的特点:
1.模块本身不被编译入内核镜像,从而控制的内核的大小。
2.模块一旦被加载,它就和内核中的其他部分完全一样。
这里我给一个简单的实现,创建了一个内核模块hello
#include <linux/init.h>
#include <linux/module.h>
static int __initdata hello_init(void)
{
printk(KERN_INFO "Hello World init\n");
return 0;
}
static void __exitdata hello_exit(void)
{
printk(KERN_INFO"Hello world exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_ALIAS("a hello module");
MODULE_AUTHOR("Yang");
MODULE_DESCRIPTION("A simple hello world modules");
这个简单的内核模块包含内核模块加载函数,卸载函数和对GPL许可权限的声明以及一些描述信息,比如模块的作者,模块的功能描述等
内核模块用于输出的的函数是内核空间的printk而非空户空间的printf,printk的用法和printf基本相似,但printk是可以定义输出级别的。
1.2 linux内核模块程序结构
一个linux内核模块主要由如下几个部分组成:
(1)模块加载函数(一般都需要)
当前通过insmod或者modprobe命令加载内核模块时,模块的加载函数自动被内核执行,完成本模块的相关初始化工作
(2)模块卸载函数(一般都需要)
当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块加载函数相反的功能。
(3)模块的许可证声明(必须)
许可证声明描述内核的许可权限,如果不声明LICENSE,模块被加载时,将收到内核被污染的警告。
(4)模块参数(可选)
模块参数是模块被加载的时候可以被传递给它的值,它本身对应的模块内部的全局变量。
(5)模块导出符号(可选)
啮合模块可以导出符号(symol,对应于函数或者变量),这样其他的模块可以使用本地模块的变量或者函数
(6)模块作者等信息声明(可选)
1.3模块加载函数
linux内核模块加载函数一般以__init标识声明,如下
static init __init hello_init(void)
{
}
module_init(hello_init);
模块记载函数必须以module_init(函数名)的形式被制定,它返回整形值,若初始化成功应返回0,,而在初始化失败时,应该返回错误的编码,在linux内核里错误编码是一个负值,在<linux/errno.h>中定义,包含-ENODEV,-ENOMEM之类的符号值,总是返回相应的错误编码是中非常好的吸光,因为只有这样,用户程序才可以利用perror等方法把它们转换成有意义的错误信息字符串。
在内核中,可以使用request_module(const char *fmt,...)函数加载内核模块
request_module(mod...)
或者
__request_module(bool wait,const char * fmt,...)
这种灵活的方式加载其他的内核模块
在linux中,所有标识符为__init的函数在连接的时候都放在.init.text这个区段内,此外所有的__init函数在区段.initcall.init中还保存了一份函数指针,在初始化时内核会通过这些函数阵阵叼哦那个这些__init函数,并在初始化完成后,释放init区段(包括.init.text, .initcall.init等)。
1.4模块卸载函数
linux内核模块卸载函数一般以__exit标识符声明
static void __exit hello_exit(void)
{
}
module_exit(hello_exit);
模块卸载函数在模块卸载的时候执行,不返回任何值,必须以"module_exit(函数名)"的形式来指定。
通常来说,模块卸载函数要完成与加载函数相反的功能,如下指示:
1.若模块记载函数注册了xxx,则模块卸载函数应该注销xxx。
2.若模块加载函数动态申请了内存,则模块卸载函数应该释放该内存。
3.若模块加载函数申请了硬件资源(中断,DMA通道,I/O端口和I/O内存等)的占用,则模块卸载函数应该释放这些硬件资源。
4.若模块加载函数开启了硬件,则卸载函数中一般要关闭之。
和__init一样,__exit也可以使对应函数在运行完成后自动回收内存。实际上,__init和__exit都是宏,其定义分别为:
#define __init __section(.init.text) __cold notrace
#ifndef __section
# define __section(S) __attribute__ ((__section__(#S)))
#endif
#define __exit __section(.exit.text) __exitused __cold notrace
#ifndef __section
# define __section(S) __attribute__ ((__section__(#S)))
#endif
数据也可以被定义为__initdata 和__exitdata,这两个宏分别为
#define __initdata __section(.init.data)
#define __exitdata __section(.exit.data)
1.5模块参数
可以用module_param(参数名,参数类型,参数读/写权限)为模块定义一个参数,例如:
static char *module_name = "dissecting linux device driver";
static int num = 4000;
module_param(num,int,S_IRUGO);
module_param(module_name,charp,S_IRUGO);
在装载内核模块时,用户可以向模块传递参数,形式为insmode(或modprobe)模块名 参数名=参数值,如果不传递,参数将使用模块内定义的缺省值。
参数类型可以是以byte,short,ushort,int,uint,long,ulong,charp(字符指针),bool或invbool,在模块被编译时将module_param中声明的类型与变量定义的类型进行比较,判断是否一致。
模块被加载后,在sys/module/目录下将出现以次模块名命名的目录,当参数读/写权限为0时,表示此参数不存在sysfs文件系统下对应的文件节点,如果此模块存在参数读/写q权限为0的命令行参数,在此模块的目录下还将出现paramteters目录,包含一系列以参数命名的文件节诶点,这些文件的权限值就是传入module_params()的参数读/写权限,而文件的内容为参数的值。
除此之外,模块也可以拥有参数数组,形式为"module_param_array(数组名,数组类型,数组长,参数读/写权限)"。
下面是一个带参数的内核模块实例
#include <linux/init.h>
#include <linux/module.h>
static char *list_name = "dissecting linux device drvier";
static int num = 4000;
static int __init hello_init(void)
{
printk(KERN_INFO "list_name :%s\n",list_name);
printk(KERN_INFO "list_name :%d\n",num);
return 0;
}
static void __exitdata hello_exit(void)
{
printk(KERN_INFO"Hello world exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
module_param(num,int,S_IRUGO);
module_param(list_name,charp,S_IRUGO);
MODULE_LICENSE("GPL");
MODULE_ALIAS("a hello module");
MODULE_AUTHOR("Yang");
MODULE_DESCRIPTION("A simple hello world modules");
1.6导出符号
/proc/kallsync文件对应着内核的符号表,它记录了符号以及符号所在的内存地址
模块可以使用如下宏导出符号到内核符号表
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);
导出符号将可以被其他的模块使用,使用前声明一下即可,EXPORT_SYMBOL_GPL()只适用于包含GPL许可权的模块,下面是例子:
#include <linux/init.h>
#incldue <linux/module.h>
MODULE_LICENSE("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);
1.7模块声明与描述
在linux内核模块中,我们可以用MODULE_AUTHOR,MODULE_DESCRIPTION,MODULE_VERSION,MODULE_DEVICE_TABLE,MODULE_ALIAS分别申明模块的作者,描述,版本,设备表和别名,例如
MODULE_AUTHOR();
MODULE_DESCRIPTION();
MODULE_VERSION();
MODULE_DEVICE_TABLE();
MODULE_ALIAS();
对于usb,pci等设备驱动,通常会创建一个MODUEL_DEVICE_TABLE,表明该驱动模块所支持的设备,代码如下:
/*对应此驱动的设备列表*/
static strct usb_device_id skel_table[]={
{USB_DEVICE(USB_SKEL_VENDOR_ID,USB_SKEL_PRODUCT_ID)}
{},/*表结束*/
};
MODUEL_DEVICE_TABLE(usb ,skel_table);
1.8模块的使用计数
模块计数管理接口try_module_get(&module)和module_put(&module)
try_module_get函数用于增加模块使用计数,若返回0表示调用失败,希望使用的模块没有被加载或正在被卸载中
module_put函数用于减少模块使用计数