您的位置:首页 > 运维架构 > Linux

LINUX kernel development之添加内核模块并加入选项菜单

2013-09-18 16:24 471 查看
一.LINUX Kernel Module

LINUX Kernel是组件模式的,所谓组件模式是指:LINUX Kernel在运行时,允许“代码”动态的插入或者移出Kernel。

所谓模块是指:相关的一些子程序,数据、入口点和出口点共同组合成的一个单一的二进制映像,也就是一个可装载的Kernel目标文件。

模块的支持,使得系统可以拥有一个最小的内核映像,并且通过模块的方式支持一些可选的特征和驱动程序。

模块可动态的插入Kernel和从Kernel中移除,提供了一种调试内核程序的简便方法。模块的加载方式分为两种:静态加载和动态加载。

静态加载是将模块直接编译入内核,若模块需要修改和升级,我们就得重新编译整个内核,并且必须重新烧写内核,工作量加大。

动态加载是需要时,加载入内核,不需要时,从内核卸载。不需对内核进行编译和烧写,就可方便的对模块进行修改和升级,大大减小了工作量,也省去了我们很多的麻烦.

二.实践

1>.Hello World:

模块的开发就像写一个应用程序,它有自己的入口点,出口点,生命周期.

/*

*hello.c Hello,World! As a Kernel Module

*/

#include <linux/init.h>

#include <linux/module.h>

#include <linux/kernel.h>

/*

*hello_init the init function , called when the module is loaded.

*Return zero if successfully loaded, nozero otherwise.

*/

static int hello_init(void)

{

printk(KERN_ALERT" Hello,World!\n");

return 0;

}

/*

*hello_exit the exit function ,called when the module is removed.

*/

static void hello_exit(void)

{

printk(KERN_ALERT"Good,Bye!\n");

}

module_init(hello_init);

module_exit(hello_exit);

MODULE_LICENSE("GPL");

MODULE_AUTHOR("HuG");

注释:

module_init()和module_exit()都是宏。

module_init()把hello_init()函数注册为这个模块的入口点。当模块被装载时,内核调用hello_init()函数。

module_init()的任务是把它的唯一参数作为相应模块的初始化函数。

初始化函数必须有如下的形式:int my_init(void)

由于初始化函数不会被外部的代码直接调用,所以,不必export这个函数。所以将初始化函数标志为static会更加的合理。

初始化函数的返回值:如果初始化成功,返回0;否则返回非零。

此处的初始化函数仅仅是打印出一句话。实际开发的模块中,初始化函数一般完成的工作是:注册资源,为数据结构分配内存等等。

同理,module_exit()是把hello_exit()函数注册为这个模块的出口点。当模块从内核中移除时,内核调用这个函数。

退出函数在返回之前,必须清除模块所占的资源,确保硬件处于一致状态等等。

退出函数必须有如下形式:void my_exit(void);同上,将函数标志为static会更合理。

注意:若采取静态加载的方式,将模块编译进内核,则内核启动时,调用static int my_init(void);但是退出函数static void my_exit(void);不会包含在内核的映像内,它也不会被调用。因为静态的加载方式,是将模块当作内核的一部分编译进内核中,所以代码永远不会从内核中删除。

宏MODULE_LICENSE()用于指定这个文件的版权许可。

MODULE_AUTHOR()用于指定本文件的作者。宏的值完全是为了提供说明信息。

2>.building Modules

在编译模块,让模块开始工作之前,我们必须确定模块的源码放置位置:

有两种方式:

一、把模块的源码增加到内核源码的一个合适的地方,即可以把文件作为一个"patch”,最终也可以将代码合到官方的源码树内。

二、在源码树之外维护和编译模块。

2.1>.在源码树内添加

我们通常的选择是将模块放入源码树之内,作为LINUX的一部分。这样模块可以生存在内核的源码树内。

如果,我们的模块是一个和USB相关的一个驱动,我们可将其放入drivers/usb/目录下。我们进入drivers/usb/gadget/目录下,我们发现gadget/目录下有很多驱动程序,都是些和USB相关的驱动。因此,我们将模块放在此目录下。

新建目录drivers/usb/gadget/hhtest/,将模块的所有文件放于此目录下(test.c+Makefile+Kconfig)

A>.test.c见hello world程序;

B>.Makefile中需写入:

obj-$(CONFIG_USB_GADGET_TEST) += test.o

C>.Kconfig写入:

config USB_GADGET_TEST

tristate "Hello Driver added by hh"
//在menuconfig界面显示的配置选项名称

default n

help

test for adding driver to menuconfig.

/×××××××××××××××××××××格式说明××××××××××××××××××××××××××××××

config USB_GADGET_TEST

tristate "Gadget Netmeeting support"

default n

help

If you say Y here,netmeeting driver will be compiles into the kernel.You can also say M here and the driver will be built as a module named netmeeting.ko

If unsure , say N.

第一行定义了配置选项。事先已经假设存在有CONFIG_前缀,因此用不着我们写。

第二行说明了这个配置选项是三态的,即有三种选择方式。第一种是选择Y,表示相对应的程序编译到Kernel之内;第二种选择是M,表示相对应的程序编译成模块;第三种选择是N,表示不编译相对应的程序。

如果没有编译成模块这个选项,可以用bool代替tristate。指令tristate后带引号的文本是配置选项名,用于各种配置实用程序的选项显示。

第三行为这个配置选项指定一个默认值,这里的默认值是选择n。即:不编译相对应的程序。

第四行help指令表示其后面是帮助文本。有助于用户和开发人员理解相应的程序和建立自己的内核。

还有一些其它的指令。

//Kconfig文件用于衔接整个Kernel源码树;

×××××××××××××××××××××××××××××××××××××××××××××××××××/

进入新建目录hhtest的父目录gadget/,分别修改其下的Makefile文件:
D>.在Makefile中添加:obj-$(CONFIG_USB_GADGET_TEST) += hhtest/

//添加目的:使build系统能够沿着源码树往下找到hhtest/子目录

E>.其次要在一个已经存在的Kconfig文件中添加如下一行(一般在新建目录的父目录的Kconfig添加):

source "drivers/usb/gadget/hhtest/Kconfig"

source命令的作用是,让后面带的文件或者目录下的文件生效。相当于,让文件执行一下,让修改生效。省去了重启电脑的麻烦。

由于我们的Kconfig是新建的,所以,它并没有生效。

所以,我们在一个已经存在的Kconfig文件中,调用source命令 , 让我们新建的Kconfig文件生效。

==================================================================================================================

源码树内添加方法{多文件}:

如果新加模块包含多个文件,则可以将Makefile(gadget/hhtest/下)写为:

obj-$(CONFIG_USB_GADGET_ONLINE) += test.o

test-objs :=one.o two.o three.o four.o

这样,test.ko由one.c,two.c,three.c,four.c四个文件编译链接而成。

如果,想要为这些文件指定额外的gcc编译选项,在Makefile文件中添加类似如下的一行:

EXTRA_CFLAGS += -ONLY_TEST

如果我们将模块的所有文件放置在目录gadget/下,则将hhtest/目录下的Makefile中的内容,添加到目录gadget/目录下的Makefile中即可;

===================================================================================================================

2.2>.置于源码树之外

如果将模块源码置于源码树之外,那么在自己的源码目录下创建Makefile文件,并且添加:

obj-m := test.o

这样会把netmeeting.c编译成netmeeting.ko。如果有多个源文件,那么在Makefile文件中添加:

obj-m := test.o

test-objs :=one.o two.o three.o four.o

放置于源码树之外和放置于源码树之内,主要区别在于build过程。

放置于源码树之外,编译模块时,需要使用命令make去找到内核源码文件和基本的Makefile文件。如下所示:

make -C /kernel/source/location SUBDIRS=$PWD modules

/keinel/source/location是已经配置过的内核源码树位置。

3、安装模块(installing Modules)

编译后的模块要放在目录/lib/modules/version/kernel/下。一般是在Makefile中添加:

modules_install:

cp netmeeting.ko /lib/modules/version/kernel/

.PHONY:

modules_install

此步操作,需要root权限。

4.生成模块依赖(Generating Modules Dependencies)

LINUX模块实用工具能够理解模块间的依赖性。

也就是说如果模块chen依赖于模块sbb,那么在装载模块chen时,模块sbb会自动的装载。

也就是LINUX下常说的依赖性编译:文件A的编译依赖于B,B的编译依赖于文件C。

在root权限下,运行如下命令来建立模块间的依赖信息。

sudo depmod 或 sudo depmod -n //-n:show process

----------------------------------------------------------------------------------------------------------------------------------------

该命令详细的使用方法和规则,可通过下面命令来查看帮助:

depmod --help

----------------------------------------------------------------------------------------------------------------------------------------

模块间的依赖信息存放在文件/lib/modules/version/modules.dep中.

----------------------------------------------------------------------------------------------------------------------------------------

5、装载模块(loading Modules)

用命令insmod装载模块是最简单的一种方法。它请求内核装载指定的模块。

命令insmod不会检查模块间的依赖关系,也不会执行是否有错误的检查。

加载模块:

insmod (模块名)

卸载模块:

rmmod (模块名)

这两个工具很简单,实用,但是缺乏智能性。

××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××

而实用工具modprobe提供了依赖性关系的解决方案,智能的错误检查和报告等。装载模块时,是我们的首选。

root权限下,执行命令:

加载模块:

modprobe (模块名) (模块参数) //模块参数见下一节6.

modprobe命令,不仅试图装载写在其后的模块,还试图装载它依赖的所有模块。因此,它是首选。

卸载模块:

modprobe r (模块名)

这里的modprobe可以移除多个模块,还可以移除它依赖的并且不在使用中的其它模块。

6、模块参数(Module Parameters)

对于如何向模块传递参数,LINUX Kernel提供了一个简单的框架。其允许驱动程序声明参数,并且用户在系统启动或模块装载时为参数指定相应值,在驱动程序中,参数的用法如同全局变量。这些模块参数也能够在sysfs中显示出来(sysfs暂不清楚是什么)。结果,有许多办法来创建和管理模块参数。

通过宏module_param()定义一个模块参数:

module_param(name,type,perm);

参数解释:

name:既是用户看到的参数名,也是模块内接受参数的变量。

type:表示参数的数据类型,是下列之一:byte,short,ushort,int,uint,long,ulong,charp,bool,invbool。

参数类型分别是:a byte, a short integer, an unsigned short integer, an integer, an unsigned integer,

a long integer, an unsigned long integer, a pointer to a char, a Boolean, a Boolean whose value is

inverted from what the user specifies.

The byte type is stored in a single char and the Boolean types are stored in variables of type int.

The rest are stored in the corresponding primitive C types.

perm:指定了在sysfs中相应文件的访问权限。

访问权限用通常的八进制格式来表示或者通常的S_Ifoo定义。

八进制格式的使用和操作系统下是一致的。

0755:表示所有制是读写、执行的权限。所在组是读和执行的权限。其他用户是读和执行的权限。

S_Ifoo下:例如:

S_IRUGO|S_IWUSR(表示其它用户具有读权限,用户具有写权限)。用0表示完全关闭在sysfs中相对应的项。

因为宏是不能声明变量的,所以,在使用宏之前,必须先声明变量。典型的用法如下:

static unsigned int use_acm = 0;

module_param(use_acm,uint,S_IRUGO);

这些变量的声明是放在模块源文件的开头部分。即use_acm是全局变量。

我们可以使用宏module_param_named()使模块源文件内部的变量名与外部的参数名有不同的名字。

可以理解为,给变量名加了个引用。

module_param_named(name,variable,type,perm);

name:外部可见的参数名

variable:源文件内部的全局变量

例如:

static unsigned int max_test = 9;

module_param_named(maximum,max_test,int,0);

如果模块参数是一个字符串时,通常使用charp类型定义这个模块参数。内核复制用户提供的字符串到内存,并且相对应的变量指向这个字符串。例如:

static char *name;

module_param(name,charp,0);

另一种方法是通过宏

module_param_string()让内核把字符串直接复制到程序中的字符数组内。

module_param_string(name,string,len,perm);

name:外部的参数名

string:内部的变量名

len:以string命名的buffer大小(len可以小于buffer的大小,但是没有意义)

perm:sysfs的访问权限(或者perm为零,表示完全关闭相对应的sysfs项)。

以上的都是只能传递一个参数给模块。如果要给模块传递多个参数,可以通过宏

module_param_array(name,type,nump,perm);

name:既是外部模块的参数名又是程序内部的变量名。name数组必须静态分配。

type:是数据类型。

perm:sysfs的访问权限。

nump:是一个指针。其值表示有多少个参数存放在数组name中。

例如:

static int finish[MAX_FISH];

static int nr_fish;

module_param_array(fish,int,&nr_fish,0444);

我们可以通过宏module_param_array_named(name,array,type,nump,perm);

参数的意义和宏module_param_named()是一样的。

最后,用宏MODULE_PARM_DESC()对参数进行说明:

static unsigned short size = 1;

module_param(size,ushort,0644);

MODULE_PARM_DESC(size,"The size in inches of the fishing pole"\

"connected to this computer");

使用这些宏,需要包含头文件:<linux/moduleparam.h>

===================================================================================================================

7.输出符号(Exported Symbols)

当装载模块的时候,模块是动态的链接入内核之中。

然而,动态链接的二进制代码只能调用外部函数,所以外部函数必须明确的输出,才能被模块调用。

在内核中,通过EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()来达到目的。

输出的函数,可以被其它的模块调用。

没有输出的函数,不能被其它模块调用。

模块比核心内核映像代码具有更严格的链接和调用规则。因为所有核心源文件链接成一个单一的作为基础的映像,因此在内核中核心代码可以调用任何非静态的接口。

当然,输出符号也必须是非静态属性。

一套输出的内核符号称之为输出的内核接口,也称之为Kernel API。

当函数声明时,用EXPORT_SYMBOL()把函数输出。

例如:

int usb_gadget_register_driver(struct usb_gadget_driver *driver)

{

.......

}

EXPORT_SYMBOL(usb_gadget_register_driver);

这样,任何模块都可以调用函数usb_gadget_register_driver(),只要在源文件中包含了声明这个函数的头文件,或者extern这个函数的声明(这点同C语言)。

若你希望你的接口让只遵守GPL的模块调用。那么通过MODULE_LICENSE()的使用,内核链接器能够保证做到这一点。

EXPORT_SYMBOL_GPL(usb_gadget_register_driver);只允许标有GPL许可证的模块访问函数usb_gadget_register_driver()。

如果你的代码配置为模块方式,那么必须确保:源文件中使用的所有接口必须是已经输出的符号,否则导致在装载时链接错误。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐