Hello World,似乎是一个摆不脱的魔咒。在最初学习C语言的时候,第一个接触的函数就跟碰上了它。今天又遇到了,在学习linux设备驱动的最初。
其实Hello World能展现给我们最基础的知识面,使我们能对新的知识马上有一个感性认识。好的,那我们在linux设备驱动说声Hello World吧,建立hello.c文件:
#include #include MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void) { printk(KERN_ALERT "Hello,world!\n"); return 0; }
static void hello_exit(void) { printk(KERN_ALERT "Goodbye,world!\n"); }
module_init(hello_init); module_exit(hello_exit); |
可以这么说,上面的代码就是linux内核模块中最简单的程序了。
1、两个函数:模块初始化函数hello_init()和模块清除函数hello_exit()。前者在模块被装载到内核时调用,后者在模块被移除时调用。这两个函数都被声明为static,因为只对内核部份可见。
2、两个宏:module_init及module_exit。module_init在模块的目标代码中增加一个特殊的段,用于说明内核初始化函数所在的位置。没有这个宏,初始化函数永远不会被调用。module_exit用来声明模块清除函数的宏。
3、两个头文件:linux/init.d,指定初始化和清除函数,如上面的两个宏就是这里面的定义的;linux/module.h,包含有可装载模块需要的大量符号和函数的定义,如MODULE_LICENSE(使用许可),MODULE_AUTHOR(声明作者),MODULE_DESCRIPTION(说明用途),MODULE_VERSION(代码修订号),MODULE_ALISA(模块别名)及MODULE_DEVICE_TABLE(用来告诉用户空间模块所支持的设备)。另补充一个头文件也是模块常用到的linux/moduleparam.h,其中的module_param()是用来创建模块参数的宏,用户可在装载模块时调整这些参数的值。
好的,接下来我们就要把它编译成能在arm9上可执行的文件。方法有二:
1、在内核树中创建模块。
将hello.c文件放入内核drivers/char子目录下,在drivers/char/Makefile中增加下面一行:
然后在内核根目录下执行“make modules”,就可以生成模块drivers/char/hello.ko。
2、在内核树外创建模块
在与hello.c的同一级目录下编辑Makefile文件如下:
然后在包含模块源代码和Makefile的目录中键入命令:
make -C ~/Documents/QQ2440/kernel-2.6.13 M=`pwd` modules |
此时会生成模块hello.ko在同一目录下。
是否感觉上面的make命令有点长,那么我们把Makefile文件修改一下,可以在内核树外的模块构 造变得更加容易。
KERNELDIR = /home/iamxzg/Documents/QQ2440/kernel-2.6.13
PWD := $(shell pwd) NFS_SHARE = /tmp/QQ2440/hello #The share directory of NFS
CC = arm-linux-gcc obj-m := hello.o
modules: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install: cp hello.ko $(NFS_SHARE)
clean: rm -rf *.o *~ *.mod.c
.PHONY: modules modules_install clean
|
接下来命令:make modules就会生成hello.ko等文件,make modules_install会把hello.ko文件复制到nfs共享目录中,make clean会清除.o,.mod.c文件。
好了,我们把hello.ko文件放到开发板中运行试一下。通过NFS把PC机硬盘挂载到开发板上,进入到挂载目录,先后命令:insmod hello.ko,rmmod hello。是否看到了Hello,world!,Goodbye,world!。哈哈,那要恭喜你成功了哦。
偷个懒,借用博客Tekkaman Ninja的心得总结:(1)驱动模块运行在内核空间,运行时不能依赖于任何函数库和模块连接,所以在写驱动时所调用的函数只能是作为内核一部分的函数。
(2)驱动模块和应用程序的一个重要不同是:应用程序退出时可不管资源释放或者其他的清除工作,但模块的退出函数必须仔细撤销初始化函数所作的一切,否则,在系统重新引导之前某些东西就会残留在系统中。
(3)处理器的多种工作模式(级别)其实就是为了操作系统的用户空间和内核空间设计的。在Unix类的操作系统中只用到了两个级别:最高和最低级别。
(4)要十分注意驱动程序的并发处理。
(5)内核API中具有双下划线(_ _)的函数,通常是接口的底层组件,应慎用。
(6)内核代码不能实现浮点数运算。
(7)Makefile文件分析:
obj-m := hello.o 代表了我们要构造的模块名为hell.ko,make 会在该目录下自动找到hell.c文件进行编译。如果
hello.o是由其他的源文件生成(比如file1.c和
file2.c
)的,则在下面加上(注意红色字体的对应关系):
hello
-objs := file1.o file2.o ......
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
其中
-C $(KERNELDIR) 指定了内核源代码的位置,其中保存有内核的顶层makefile文件。
M=$(PWD)
指定了模块源代码的位置
modules目标指向obj-m变量中设定的模块。
(8)insmod使用公共内核符号表来解析模块中未定义的符号。公共内核符号表中包含了所有的全局内核项(即函数和变量的地址),这是实现模块化驱动程序所必须的。
(9)Linux使用模块层叠技术,我们可以将模块划分为多个层,通过简化每个层可缩短开发周期。如果一个模块需要向其他模块到处符号,则使用下面的宏:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
|
符号必须在模块文件的全局变量部分导出,因为这两个宏将被扩展为一个特殊变量的声明,而该变量必须是全局的。
(10)所有模块代码中都包含一下两个头文件:
#include <linux/init.h>
#include <linux/module.h>
|
(11)所有模块代码都应该指定所使用的许可证:
MODULE_LICENSE("Dual BSD/GPL");
|
此外还有可选的其他描述性定义:
MODULE_AUTHOR("");
MODULE_DESCRIPTION("");
MODULE_VERSION("");
MODULE_ALIAS("");
MODULE_DEVICE_TABLE("");
|
上述MODULE_
声明习惯上放在文件最后。
(12)初始化和关闭
初始化的实际定义通常如下:
static int _ _ init initialization_function(void)
{
/*初始化代码*/
}
module_init(initialization_function)
|
清除函数的实际定义通常如下:
static int _ _exit cleanup_function(void)
{
/*清除代码*/
}
module_exit(cleanup_function)
|
(13)
Linux内核模块的初始化出错处理一般使用“goto”语句。通常情况下很少使用“goto”,但在出错处理是(可能是唯一的情况),它却非常有用。在
大二学习C语言时,老师就建议不要使用“goto”,并说很少会用到。在这里也是我碰到的第一个建议使用“goto”的地方。“在追求效率的代码中使用goto语句仍是最好的错误恢复机制。”--《Linux设备驱动程序(第3版)》以下是初始化出错处理的推荐代码示例:
struct something *item1;
struct somethingelse *item2;
int stuff_ok;
void my_cleanup(void)
{
if (item1)
release_thing(item1);
if (item2)
release_thing2(item2);
if (stuff_ok)
unregister_stuff();
return;
}
int __init my_init(void)
{
int err = -ENOMEM; item1 = allocate_thing(arguments); item2 = allocate_thing2(arguments2);
if (!item2 || !item2)
goto fail; err = register_stuff(item1, item2);
if (!err)
stuff_ok = 1;
else
goto fail; return 0; /* success */
fail: my_cleanup( ); return err;
}
|
(14)模块参数:内核允许对驱动程序指定参数,而这些参数可在装载驱动程序模块时改变。
以下是我的实验程序:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("Dual BSD/GPL");
static char *whom = "Tekkaman Ninja";
static int howmany = 1;
static int TNparam[] = {1,2,3,4};
static int TNparam_nr = 4;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);
module_param_array(TNparam , int , &TNparam_nr , S_IRUGO);
static int hello_init(void)
{
int i;
for (i = 0; i < howmany; i++)
printk(KERN_ALERT "(%d) Hello, %s !\n", i, whom);
for (i = 0; i < 8; i++)
printk(KERN_ALERT "TNparam[%d] : %d \n", i, TNparam[i]);
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, Tekkaman Ninja !\n Love Linux !Love ARM ! Love KeKe !\n");
}
module_init(hello_init);
module_exit(hello_exit);
|
实验结果是 :
[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#ls
cs89x0.ko hello.ko prism2_usb.ko
hello-param.ko p80211.ko
[Tekkaman2440@SBC2440V4]#insmod hello-param.ko howmany=2 whom="KeKe" TNparam=4,3,2,1
(0) Hello, KeKe !
(1) Hello, KeKe !
TNparam[0] : 4
TNparam[1] : 3
TNparam[2] : 2
TNparam[3] : 1
TNparam[4] : 1836543848
TNparam[5] : 7958113
TNparam[6] : 1836017783
TNparam[7] : 0
[Tekkaman2440@SBC2440V4]#insmod hello-param.ko howmany=2 whom="KeKe" TNparam=4,3,2,1,5,6,7,8 TNparam: can only take 4 arguments hello_param: `4' invalid for parameter `TNparam' insmod: cannot insert 'hello-param.ko': Invalid parameters (-1): Invalid argument [Tekkaman2440@SBC2440V4]#
|
我这个实验除了对参数的改变进行实验外,我的一个重要的目的是测试“
module_param_array(TNparam , int , &TNparam_nr , S_IRUGO);
”中&TNparam_nr
对输入参数数目的限制作用。经过我的实验,表明&TNparam_nr
并没有对输入参数的数目起到限制作用。真正起到限制作用的是“static int TNparam[] = {1,2,3,4};
”本身定义的大小,我将程序进行修改:
static int TNparam[] = {1,2,3,4};
改为 static int TNparam[] = {1,2,3,4,5,6,7,8};
其他都不变。
编译后再进行实验,其结果是:
[Tekkaman2440@SBC2440V4]#insmod hello-param.ko howmany=2 whom="KeKe" TNparam=4,3,2,1,5,6,7,8
(0) Hello, KeKe !
(1) Hello, KeKe !
TNparam[0] : 4
TNparam[1] : 3
TNparam[2] : 2
TNparam[3] : 1
TNparam[4] : 5
TNparam[5] : 6
TNparam[6] : 7
TNparam[7] : 8
[Tekkaman2440@SBC2440V4]#
|
(15)“#include <linux/sched.h
>” 最重要的头文件之一。包含驱动程序使用的大部分内核API的定义,包括睡眠函数以及各种变量声明。
(16)“#include <linux/version.h
>” 包含所构造内核版本信息的头文件。
阅读(1846) | 评论(0) | 转发(0) |