一、第一个Linux设备驱动程序——hello world模块
Hello.c文件:
#include <linux/init.h> #include <linux/module.h>
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,lingd\n"); }
module_init(hello_init); module_exit(hello_exit);
|
说明:
MODULE_LICENSE是用来告知内核,该模块带有一个自由的许可证
module_init用于告知内核在加载模块时,调用hello_init函数对模块进行初始化
module_exit用于告知内核在卸载模块时,调用hello_exit函数对模块进行必要的清理工作
printk是内核的打印函数,与C库函数printf类似。内核在运行时,没有 C 库的帮助,因此需要有自己的打印函数。而模块能够调用 printk 是因为,在 insmod 加载了它之后,模块被连接到内核并且可存取内核的公用符号 (函数和变量)。
KERN_ALERT字符串指明了消息的优先级,注意:KERN_ALERT后面是没有逗号的。
Makefile文件:
KERNELDIR=/home/lingd/arm2410s/linux-2.6.24.7
#The current directory is passed to sub-makes as argument PWD:=$(shell pwd) INSTALLDIR=/home/lingd/arm2410s/modules CROSS_COMPILE=/opt/crosstool/gcc-4.1.1-glibc-2.3.2/arm-linux/bin/arm-linux- CC= $(CROSS_COMPILE)gcc
obj-m := hello.o
modules: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install: cp hello.ko $(INSTALLDIR)
clean: rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
|
说明:
1、KERNELDIR为内核源代码所在的目录
2、PWD为当前目录
3、INSTALLDIR为模块安装路径
4、CROSS_COMPILE为交叉编译工具的前缀
5、obj-m := hello.o 表示了我们要构造的模块名为hello.ko,make 会在该目录下自动找到hello.c文件进行编译。如果hello.o是由其他的源文件生成(比如file1.c和file2.c)的,则在其后面加上(注意红色字体的对应关系):
hello-objs := file1.o file2.o ......
5、$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
其中-C $(KERNELDIR)指定了内核源代码的位置(其中保存有内核的顶层Makefile文件),M=$(PWD) 指定了模块源代码的位置,modules目标指向obj-m变量中设定的模块。
编译hello.c
[root@localhost hello]# make
make -C /arm2410s/kernel-2.6.24.7 M=/arm2410s/lingd/hello modules
make[1]: Entering directory `/arm2410s/kernel-2.6.24.7'
CC [M] /arm2410s/lingd/hello/hello.o
Building modules, stage 2.
MODPOST 1 modules
LD [M] /arm2410s/lingd/hello/hello.ko
make[1]: Leaving directory `/arm2410s/kernel-2.6.24.7'
[root@localhost hello]# make modules_install
cp hello.ko /arm2410s/modules
将hello.ko下载到开发板上运行
/tmp/modules # ls
hello.ko
/tmp/modules # insmod hello.ko
Hello,world
/tmp/modules # lsmod
Module Size Used by
hello 1280 0
/tmp/modules # rmmod hello
Goodbye,lingd
/tmp/modules # lsmod
Module Size Used by
二、内核模块与应用程序的区别:
1、大部分应用程序从始至终都处理某个的任务;内核模块只是为了响应将来的服务请求而加载注册自己到内核中,并且其初始化函数执行完后就立刻结束(也就是说模块初始化函数只是为以后调用模块函数作准备,只是告诉内核该模块能做什么);
2、应用程序结束运行时可以不进行资源清理,但要是模块退出函数不对资源(模块初始化函数建立的资源及其他需要处理的资源)进行清理,那么就会残留一些东西在系统中(除非你重启系统);
3、应用程序可以调用库函数,只要它在连接时将相应的库连接到程序就OK了。但是模块只是连接到内核中,没有连接到库,它只能调用在内核中已定义的函数,如printk;内核中定义的函数与数据结构见内核源码中的include目录下的文件;
4、处理错误的方式不同。在应用程序开发中段错误是无害的,调试器可以帮助我们追踪错误发现源码中的问题;而一个内核错误,如果没有导致系统崩溃,也至少会杀掉当前进程。
5、用户空间和内核空间
Unix 系统利用了处理器的多种级别(工作模式)这一硬件特性,但只使用了最高级别和最低级别。在 Unix 下,内核在最高级运行( 也称之为超级模式、内核模式 ),这里任何事情都允许;而应用程序在最低级运行(所谓的用户模式),这里处理器控制了对硬件的直接存取以及对内存的非法存取。
模块的作用是扩展内核的功能; 模块化的代码在内核空间运行。设备驱动由设备服务子程序(系统调用的一部分执行)和中断处理程序组成。而应用程序运行在用户空间。
6、内核并发问题
除了多线程应用程序,大部分应用程序,都是顺序执行的。但由于硬件环境和Linux系统特点,驱动程序设计必须充分考虑并发问题。Linux 多进程特点导致:在同一时间,可能不止一个进程能够试图使用你的驱动;大部分设备能够中断处理器和中断处理的异步运行又导致:可能在你的驱动试图做某些事情的同一时间又被调用;而Linux 在对称多处理器系统( SMP )上运行,又将导致:你的驱动可能在多个 CPU 上并发执行。最后,在2.6内核中,内核代码也是可抢占的了。这个变化使得即便是单处理器会有许多与多处理器系统同样的并发问题。由于以上这些情况,导致了 Linux 内核代码,包括驱动代码,必须是可重入的,即它必须能够同时在多个上下文中运行。
7、应用程序存在于虚拟内存中,有一个非常大的堆栈区。而内核,相反,有一个非常小的堆栈;它可能小到一个只有4096 字节的页。你的函数必须与整个内核空间调用链共享这个堆栈。因此,在驱动程序中,如果你需要大型数据结构,你应当选择动态分配空间,而不是去定义一个巨大的自动变量。
其他值得注意的问题:
1、内核获取当前进程信息
尽管内核模块不像应用程序一样顺序执行,但内核所做的大部分事情都是代表一个特定进程的。内核代码可以通过宏current(具体定义见 )来引用当前在运行的进程,它产生一个指针指向结构 task_struct。
#include
static inline struct task_struct *get_current(void) { return current_thread_info()->task; }
#define current (get_current())
|
2、内核API中具有双下划线__的函数,通常是接口的底层组件,应慎用。
3、内核代码不支持浮点运算。
三、模块加载和卸载
1、insmod加载模块
insmod原理:insmod通过 kernel/module.c 中定义的系统调用sys_init_module ,来分配存放模块的内核内存(这个内存用 vmalloc 分配);接着,sys_init_module拷贝模块的代码段到这块内存区, 并借助内核符号表解决模块中的内核引用(内核符号表中包含了所有的全局内核项,即函数和变量的地址,这是实现模块化驱动程序所必须的)并且调用模块的初始化函数注册和初始化模块。
2、modprobe加载模块
modprobe, 也用来加载一个模块到内核。它与insmod的不同在于:它会查看要加载的模块, 是否引用了当前内核没有定义的符号. 如果发现有, 则modprobe 在当前模块搜索路径中寻找定义了这些符号的模块,并将这些模块一并加载到内核中. 如果在这种情况下你使了 insmod 会导致模块加载失败,并在系统日志文件中留下一条 " unresolved symbols "消息.
3、rmmod卸载模块
当模块还在使用或者内核被配置成不许模块卸载时,rmmod会失败。内核允许强行卸载模块,但不建议这么做。
4、lsmod查看系统已加载的模块
系统已加载模块的信息可以在/proc/modules和/sys/module/sysfs中找到.
四、版本与平台依赖问题
内核模块非常依赖于内核的数据结构和函数原型,但各版本内核的数据结构和函数接口可能会有较大差别。内核代码会根据相应平台的特性进行优化。对于不同体系构架的平台,内核所作的优化都是不同的。因此,对不同版本的内核,不同的平台,内核模块都需要重新编译。
所有的可加载模块包含一个链接步骤。这个链接步骤将内核代码树中的init/vermagic.o文件链接到了模块。这个.o文件给可加载模块提供了一个特殊的段(也可以成为节),在其中描述了模块的构建环境,包括:使用的编译器版本、内核是否构建为SMP、内核抢占是否使能、编译的体系结构(如i386、ARM等)、内核版本。一个模块的这些项里如果有任何一项与kernel不同,就会致使这个模块与运行的kernel不兼容。新的模块加载器将检测这些不兼容性并且拒绝加载模块
如果你想编写一个可以在多个内核版本上工作的模块(特别地是如果它必须跨大的发行版本), 那你只能使用宏和 #ifdef 来编写你的代码. 在linux/version.h 定义一些可用于版本检查的宏(这个头文件已包含在 linux/module.h里)
UTS_RELEASE:该宏为当前内核版本编号的字符串表示方式
#define UTS_RELEASE "2.6.24"
LINUX_VERSION_CODE:该宏为内核版本编号的二进制表示方式,主版本号、此版本号及修订版本号各占1byte,如2.6.24为132632(0x020618)。
#define LINUX_VERSION_CODE 132632
KERNEL_VERSIONKERNEL_VERSION(major,minor,release):该宏用于将内核版本编号转换成一个整型值。
#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))
大部分的内核版本的依赖性可以利用预编译条件及KERNEL_VERSION 和 LINUX_VERSION_VODE来解决. 但是繁多的 #ifdef 条件会搞乱驱动的代码; 最好的办法还是把它们限制到特定的头文件. 将明显版本(或者平台)依赖的代码应当隐藏在一个低级的宏定义或者函数后面. 高层的代码就可以只调用这些函数, 而不必关心低层的细节. 这样书写的代码易读并且更健壮.
五、内核符号表
内核符号表包含了搭建模块化驱动所需要的全局内核项(包括函数和变量)的地址。模块加载时,模块输出的符号也会加入到内核符号表中。通过借助内核符号表,新的模块可以用你的模块输出的符号, 你也可以层叠新的模块在其他模块之上.使用层叠技术,我们可以将模块划分成多个层(子模块),通过简化每一层来缩短开发时间.
当使用层叠的模块时, 熟悉 modprobe 命令是有很有用的(前面已经介绍过,不再多说)。
通过下面两个宏可以实现符号输出:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
说明:_GPL 版本的宏定义只能使符号对 GPL 许可的模块可用. 符号必须在模块文件的全局变量部分导出,因为这两个宏将被扩展为一个特殊变量的声明,而该变量必须是全局的。这个变量存储在模块的一个特殊的可执行部分( 一个 ELF 段 ), 内核在加载模块时通过这个部分找到模块输出的变量. ( 感兴趣可以看 linux/module.h )
六、基础知识
1、所有模块代码中都包含一下两个头文件
#include
#include
moudle.h 包含了大量的可加载模块需要的函数和符号的定义. init.h包含了module_init和module_exit两个宏的定义。为了支持在模块加载时传递参数给模块,大部分模块还包含 moudleparam.h文件。
2、所有模块代码都应该指定所使用的许可证:
MODULE_LICENSE("Dual BSD/GPL");
此外还有可选的其他描述性定义:
MODULE_AUTHOR(""); //模块编写者
MODULE_DESCRIPTION(""); //模块描述
MODULE_VERSION(""); //模块版本号
MODULE_ALIAS(""); //模块别名
MODULE_DEVICE_TABLE(""); //模块支持的设备
上述MODULE_声明习惯上放在文件最后。
3、注册函数以 register_ 做前缀、系统调用的名子以 sys_ 为前缀
4、初始化函数和退出函数
初始化函数定义常常如下:
static int __init initialization_function(void)
{
/* Initialization code here */
}
module_init(initialization_function);
退出函数定义通常如下:
static void __exit cleanup_function(void)
{
/* Cleanup code here */
}
module_exit(cleanup_function);
初始化函数和清理函数应当声明成静态的, 因为它们没有必要在模块文件之外可见。声明中的 __init 标志是告知内核该函数只是在初始化时使用. 模块加载者在模块加载后会释放掉初始化函数所占的内存,以做其他用途. 类似的还有__initdata(只在初始化时用的数据)。__exit标识这个函数只用于模块卸载( 通过使编译器把它放在特殊的 ELF 段). 如果你的模块直接建立在内核里, 或者如果你的内核配置成不允许模块卸载, 标识为 __exit 的函数会被简单地丢弃掉. __exit 标识的函数只有在模块卸载或者系统关闭时才会调用。
以上这些都不是强制性要求的,你可以选择不要。但module_init、module_exit是强制性要求的。这两个宏在模块目标代码中增加了特别的段, 用于表明在哪里找到模块的初始化函数和清理函数. 如果没有了module_init、module_exit的指明, 你的初始化函数和清理函数将永远不会被调用.
初始化函数失败时,返回错误代码;否则返回0.
模块清理函数必须撤销任何由初始化函数进行的注册, 并且通常(但常常不是要求的)是按照注册时相反的顺序注销设施.
5、初始化中的错误处理
以前老师常要求我们避免使用goto语句,但在出错处理时它却非常有用。
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; }
|
注意:清理函数,需要被非退出代码调用时,不能声明为__exit。
6、模块竞争
需要注意的两点:
a、内核的某些部分会马上使用任何你刚注册完的设施. 换句话说, 在你的初始化函数仍在运行时,内核完全可能已经开始调用你的模块了. 所以,一旦完成了注册,你的代码就必须准备好被调用。在那个设施需要支持的所有的内部初始化工作还没完成前.不要注册它
b、你也必须考虑到如果你的初始化函数中途失败,但是内核的某部分已经在使用你的模块已注册的设施,这时会些发生什么. 如果在你的模块中,这种情况是可能的, 你应当认真考虑让初始化不可能失败. 毕竟, 模块已成功输出一些有用的东西. 如果初始化必须失败, 那你就必须小心处理任何可能在内核其他地方发生的操作, 直到这些操作已完成.
7、模块参数
内核允许对驱动程序指定参数,而这些参数可在insmod、modprobe驱动程序模块时改变。Modprobe还可以从自己的配置文件/etc/modprobe.conf读取这些参数的值。
#include
module_param(name, type, perm);
module_param_array(name,type,num,perm);
说明:
module_param用于创建模块参数, 该参数可以在模块加载时被调整.
name为变量或数组的名称
type可以是 bool, charp, int, invbool, short, ushort, uint, ulong, intarray.
num为模块参数的数目,不得大于数组的size
perm是用于设置权限,需要使用linux/stat.h中定义的权限值来初始化。perm控制谁可以在 sysfs 中存取这些模块参数. 如果 perm 被设为 0, 参数不会保存在sysfs 中. 否则, 参数出现在 /sys/module/sysfs 下面, 并带有给定的权限. perm 被设为 S_IRUGO 时,参数可以被任何人读取, 但不能被改变; S_IRUGO|S_IWUSR 允许 root 改变参数. 注意, 如果在 sysfs 中的参数被修改了, 你的模块的参数值也会跟着改变了, 但是你的模块不会得到通知. 你不应当使模块参数可写, 除非你准备好检测这个改变并作出相应反应.
设置模块参数的一个例子。该例子展示了如何在模块加载时设置模块参数,并测试array_nr对数组参数个数的限制作用。实验证明:array_nr并没有对数组参数个数起限制作用,真正起限制作用的是array本身的大小。注意,源码中array、array_nr的定义及函数hello_init中的第二个循环。
hellop.c文件:
#include <linux/init.h> #include <linux/module.h> #include <linux/moduleparam.h> MODULE_LICENSE("Dual BSD/GPL");
static char *who = "lingd"; static int howmany = 1; static int array[] = {1,2,3,4,5}; static int array_nr = 3;
module_param(howmany, int, S_IRUGO); module_param(who, charp, S_IRUGO); module_param_array(array , int , &array_nr , S_IRUGO);
static int hello_init(void) { int i; for (i = 0; i < howmany; i++) printk(KERN_ALERT "(%d) Hello, %s !\n", i, who); for (i = 0; i < 5; i++) printk(KERN_ALERT "array[%d] : %d \n", i, array[i]); printk(KERN_ALERT "array size : %d \n", sizeof(array)/sizeof(int));
return 0; }
static void hello_exit(void) { printk(KERN_ALERT "Goodbye,lingd\n"); }
module_init(hello_init); module_exit(hello_exit);
|
编译hellop.c后,下载到开发板上运行
/tmp/modules # insmod hellop.ko howmany=3 who="jevons" array=0,9,8,7,6
(0) Hello, jevons !
(1) Hello, jevons !
(2) Hello, jevons !
array[0] : 0
array[1] : 9
array[2] : 8
array[3] : 7
array[4] : 6
array size : 5
/tmp/modules # rmmod hellop
Goodbye,lingd
将源码中array定义改为static int array[] = {4,5},重新编译后,下载到开发板上运行
/tmp/modules # insmod hellop.ko howmany=3 who="jevons" array=2,8,9
array: can only take 2 arguments
hellop: `2' invalid for parameter `array'
insmod: cannot insert 'hellop.ko': invalid parameters
/tmp/modules # lsmod
Module Size Used by
/tmp/modules # insmod hellop.ko howmany=3 who="jevons" array=3,6
(0) Hello, jevons !
(1) Hello, jevons !
(2) Hello, jevons !
array[0] : 3
array[1] : 6
array[2] : 2
array[3] : -1006708242
array[4] : 3
array size : 2
/tmp/modules # rmmod hellop
Goodbye,lingd
在实际编码中,应该使array_nr与array实际大小相等,以增加代码可读性和避免因array_nr与array实际大小不相等引起的错误
8、#include
最重要的头文件之一. 这个文件包含很多驱动使用的内核 API 的定义, 包括睡眠函数和许多变量声明.
9、#include
包含在内核版本信息.