`
helloyesyes
  • 浏览: 1271405 次
  • 性别: Icon_minigender_2
  • 来自: 武汉
文章分类
社区版块
存档分类
最新评论

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

阅读更多

  这是一个很简单的小例子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,以判断是否真正初始化结束。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics