您的位置:首页 > 编程语言

kernel module编程(一):建立一个小例子

2009-07-23 10:20 113 查看
  这是一个很简单的小例子hello world。也是《Linux Device Drivers》一书第二章的书读笔记。

需要开发环境

  为了使得内核模块可以编译,我们需要安装kernel-devel的rpm包,例如在Fedora,我们需要yum install kernel-devel。如果使用的linux版本没有提供自动安装这个包,我们需要编译linux kernel来获得。这个包的相关文件放置在/usr/src/kernel/<uname -r>下面,他是kernel tree。

小程序

  我们建立hello.c文件,下面是内容。黑体的部分是小程序主体部分,这部分已经足够作为一个完整的模块小程序,其他颜色是我们的一些试验。暗红色部分是处理携带参数加载模块的部分

#include <linux/init.h>

#include
<linux/module.h>

#include
<linux/sched.h>

MODULE_LICENSE("Dual BSD/GPL");

static char * whom = "world";

statci int howmany = 1;

module_param(howmany,int,S_IRUGO);

module_param(whom,charp,S_IRUGO);



/* EXPORT_SYMBOL(xxxx);

*/

static int __init hello_init(void)

{

int i = 0;

printk("Hello
world enter/n");

for(i = 0 ; i < howmany; i++)


printk("Hello, %s/n", whom");



/** 下面是KERN_ALERT的试验,这是一个macro,相当于字符串"<1>",注意请不要在后面加上逗号。*/

printk(KERN_ALERT "Hello world module loaded!/n");

/** 测试通过current(struct task_struct)获取当前进程名字和进程号,需要头文件linux/sched.h *



** KERN_INFO相当于字符串"",即不显示任何内容,然而我们建议在程序中注明,以表示信息的

*



**

级别,使程序更为规范和可读 */



printk(KERN_INFO "The Process is [%s], PID [%i]/n".current->comm,current->pid);

return 0;

}

static void __exit hello_exit(void)

{

printk("Hello
world exit!/n");

}

module_init(hello_init);

module_exit(hello_exit);

  头文件module.h中定义了很多用于加载模块的symbol和参数,init.h是用于初始化和cleanup函数,这两个头文件是模块编程中所必须的。另外moduleparam.h用于在模块加载的过程中传递参数,也是常用的头文件。

  

module_init和module_exit用来kernel macros来表明模块加载时和卸载时的触发。这两个函数都是static,只用一次,因为不可能被其他文件所调用。
在module_init的类型为__init,这个表明这个函数只用于初始化,初始化完后,他的空间将被回收,可以用__initdata来表示只在初始化中使用数据,也将会被回收。一般的应用程序,在结束的时候,不需要仔细地去是否资源,因为进程终结,系统会自动进行。但是在kernel模块中,exit需要仔细地去删除所有init中建立或分配的资源,否则直到系统结束(例如reboot)这些资源才会释放,同样我们注意到这个类型为__exit,只用于unload。

  

还有一个特别的宏定义MODULE_LICENSE来表明license,如果没有设定,那么在加载的时候,系统会提示:hello: module license 'unspecified' taints kernel。可以是"GPL v2","GPL and additional rigths"等等,
如果不打算进行公开,可以使用"Proprietary"。如果需要其他的一些宏,例如MODULE_AUTHOR(), MODULE_VERSION(), MODULE_DESCRIPTION()等等,这些可以在linux/module.h中查看。

  kernel模块不允许链接函数,他只允许使用kernel定义的函数,include放置在/usr/src/kernel/${shell uname -r}中。对于OS,kernel模块在kernel space中运行,而应用模块在user space运行。一个应用通过发出一个system call或者他由一个硬件中断所挂起是,可以从用户空间到内核空间。kernel module的错误可能会引起系统的崩溃,影响当前的进程,和普通程序不一样。

  

在上面的小例子中,有一个被注释掉的EXPORT_SYMBOL(name),也可能是EXPORT_SYMBOL_GPL(name)。在内核模块中是没有链接啊或者库的说法,这并不是说明内核模块之间就是平面的结构,一个模块可以利用另一个模块的Symbol,这个在设备驱动中常见,例如很多设备都是基于USB的,那么他们可以使用usbcore,input模块中的symbol,如果我们的模块也希望可以被其他模块引用,需要使用上述的两种方式表明,而_GPL表示只能用于具有GPL版权的模块。这些Symbol都是全局的。

  内核模块可以携带参数调用,例如设备驱动可以携带一些与系统硬件有关的信息。参数说明为module_param(name, type, perm)以及数组module_param_array(name, type, num, perm)。type是类型,例如:bool, invbool, charp, int, long, short, uint, ulong, ushort。num实际是array的长度。perm是permission,相关的值可以在linux/stat.h中查到。S_IRUGO表示只读,可读可写表示为S_IRGO|S_IWUSR,0表示没有sysfs。在调用的时候,我们可以通过insmod hello.ko whom="Mom" howmany=5来给出参数,我们可以不给出参数,采用缺省值,也可以只给出其中的一个参数。

编译

  然后建立Makefile文件:

#Makefile 2.6

#表示模块是从hello.o建立的,名字为hello.ko

obj-m := hello.o

#module-objs:如果模块由N个文件组成,那么其他文件就应该描述如下:module-objs:= file1.o file2.o,由于我们的模块叫做hello,在这个例子中应该写为hello-objs



hello-objs :=

PWD := $(shell pwd)

KDIR := /lib/modules/$(shell uname -r)/build


all:



#-C表示kernel source目录,在/lib/modules/<uname -r'>/build,在那里可以找到kernel的最高lenvel的makefile,M=表示在建立模块target的时候,makefile回归到我们模块程序的目录。



make -C $(KDIR) M=$(PWD) modules

clean :

make -C $(KDIR) M=$(PWD)
clean

  如果我们有多个c文件,可以在test-objs参数中加入他们的obj文件。接下来就是make了,编译后,生成hello.o文件和hello.ko,还有hello.mod.c及其obj文件,Module.markers
Module.sysvers Modules.order
文件。

  我们对多个*.c文件情况做一个说明:我们希望创建一个模块的名字叫做hello,我们有三个*.c文件,分别为hello.c, file1.c和file2.c。这样是有问题的,因为在Makefile中obj-m := hello.o,这是指定模块的名称, hello-objs := file1.o file2.o hello.o,这里是说hello模块包括的的obj文件,如果我们在里面不填写hello.o,那么实际并没有编译hello.c,而是在CC[M] file1.o和file2.o,通过LD[M]得到模块hello.o,如果我们在这里填写了hello.o,那么在obj-m和hello-objs中都含有hello.o,对make来讲会产生循环和混淆,因此也不能这样书写。如果我们由多个C文件来构造一个模块,那么C文件的名字不能和模块名字一样,
在这个例子中我们可以将hello.c改名为hello_main.c,在Makefile中obj-m := hello.o,hello-objs = file1.o file2.o hello_main.o。

加载和卸载

  加载这个模块,可以使用insmod或者modprobe,运行lsmod,可以看到已经加载,通过rmmod命令来进行卸载,这些命令需要具备root的权限。
和imsnod相比,modprobe会查看被下载的模块里面的symbols有没有在kernel中没有定义的,如果发现有没定义的, 他将在该模块的路径下找寻其他模块是否含有这些定义,如果含有将他们也一并load。对于这种情况insmod或者modprobe没有找到匹配的模块,将会报告:unresolved symbols。

  使用lsmod当前加载的模块,实际相当于cat /proc/modules。

  如果想查看printk的结果,系统输出在console,如果我们使用init 3进入我们的系统, 即使用命令行方式,那么我们可以直接看到输出的结果,如果我们是在图形界面中的terminal或者console,又或者是通过远程访问的方式,这个输出是看不到的,

可以使用dmesg命令来查看。也可以在/var/log/messages中查看。一般调测,我喜欢使用dmesg。

和Kernel版本匹配的问题

  

我们需要注意的内核模块和kernel是密切相关的,如果我们试图加载一个模块但是不能和kernel兼容,则报告类似下面的错误:Error inserting './hello.ko': -1 Invalid module format。这是有kernel中vermagic.o来判定的。

  

由于每个版本kernel的改动都会影响我们,如果我们需要一个模块同时能在多个版本中工作,维护源代码的一致性,需要加上#ifdef之类的定义,可以使用linux/utsrelease.h下面的UTS_RELESE,liux/version.h下面的LINUX_VERSION_CODE或者KERNEL_VERSION。后两者将他们值转换为0x的16进制格式
,比较清晰。我们说的只是源代码的一致性,仍需要指定不同的KDIR来进行编译。对于这种情况,根据我们的编程思想,我们将low-level的代码,需要通过宏来指定的部分,和high-level的代码,相对和OS版本独立,或者能够通过底层代码屏蔽差异的部分分开,这样程序变得易读,容易维护。如果作为一个商业版本进行发布,如果需要面临不同的OS版本,或者OS以后升级的不同的版本,最好的方式能过进入upstream的official linux kernel,否则作为 GPL,公布source,允许在不同的版本中用户自行编译,我们也可以将这些步骤***成小工具发布。如果提供的是binary的方式,应当指明kernel的版本,应考虑到以后OS升级的问题。

在初始化过程中出现错误的处理

  

由于某些原因,在初始化的过程会出现一些错误。如果出现在register的过程,我们可以选择让初始化继续进行,降低模块提供的功能,这是通常的做法如果我们认为错误严重无法加载模块,为了释放kernel空间,我们应该清除所有在错误之前的注册。

  
在通常的程序中,goto是不受欢迎的,但是它并没有在C中删除,而在模块编程中,使用goto来处理错误反而会减少复杂度,增加可读性,因此在内核中,goto常用于处理错误。下面是一个例子:

struct st1 * item1;

struct st2 * item2;

int stuff_ok;

/** 我们注意到这里没有使用__exit,因为这个函数不会只在unload的时候调用,这点需要注意。*/

void my_cleanup(void)

{

if(item)



release_func(item1);

if(item2)

release_func2(item2);

if(stuff_ok)

unregister_stuff();

return;

}



int __init my_init(void)

{

int err = -ENOMEN;

item1 = allocate_func(arg);

item2 = allocate_func2(arg2);

if(!item1 || !item2)

goto fail;

err = register_stuff(item1, item2);

if( ! err)

stuff_ok =1;

else

goto fail;

/** 0表示成功,其他的错误代码可以在linux/errno.h中查获,当然我们也可以自行定义*/

return 0;

fail:

my_cleanup();

return err;

}

  如果我们register多个,顺序分别为A、B、C,那么通常在unregister的过程中,顺序为C、B、A。在初始化过程中,一旦注册,就可能慢上被调用(call),即使初始化函数没有执行完,因此要在注册前将必要的初始化完成。初始化失败,但是已经注册也有可能被调用,这些我们都必须考虑。简单的处理可以设置tag,当初始化成功后,tag置位,触发函数检查tag,以判断是否真正初始化结束。



相关链接:

我的与kernel module有关的文章

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐