|
配置环境
开始写什么之前,我们先把环境搭好吧。《linux 设备驱动程序》使用的是2.6.10的内核,不过没有关系,我们使用2.6.X的内核在机制上是相同的。基于2.6内核的操作系统上,我个人认为suse比fedora 好。至于其他的嘛,debian也不错。我使用的是suse 10.2的系统,您可以从 http://downsload.opensuse.org 来下载。Suse 10.2是基于2.6.18的内核的,比较新而且编译起来也没什么问题。如果您希望得到一个标准的内核那么请访问 http://www.kernel.org。
Jon建议使用标准版的内核源码来编译你自己的内核程序,然而,如果内核被修改而来适应某个特性。我们使用标准版的内核,编译的目标码确并不一定能够正常的在系统中运行。所以,对于我们学习的来说,使用操作系统里自带的就可以了。如果以后正式开发则需要使用标准内核,然后拿到个体机器的时候,再用特殊内核的源码来编译就好了。
安装的时候注意有个选项是让我们来编译内核的,把它选上。如果没有选也没有关系,进入系统之后把安装盘放进去。找到 kernel_source的rpm包安装它就可以了。安装完之后,应该是在/usr/src/Linux的目录下。Linux 这个目录是个链接,它指向Linux 的真正目录。一般这个是个带版本号的目录,比如Linux2.6.18.2。如果我们安装了多个版本的内核,那么我们就可以改变Linux 的指向从而来用统一的代码来访问不同内核了。
我习惯上在安装完了之后把.加到PATH中去。你可以修改/etc/profile这个文件来达到这个目的。最好对linux的启动过程有个了解。注意 rc.local 、profile、init.d这几个目录和文件里的内容关注一下。它们负责初始化系统,执行检查和进程的运行。
Suse10。2中带了 gcc 4.1的编译器,我们最好安装一下Kdevelop c/c++。这个程序可以用来编写我们的驱动模块。当然有很多人喜欢使用gedit 来编写代码,然后编写Makefile,最后编译。当然这样做是有好处的,不过想我这样的懒人,还是使用autogen来产生 Makefile吧。打开终端,键入 gcc –v可以看到你当前的gcc版本。你可以安装gcc的其他版本,不过请记住,gcc是个特殊的程序,它和系统紧密联系着。更换gcc有可能产生不可预料的结果,所以更换之前请做好备份。我的做法是将gcc改名为gcc41然后做一个连接gcc到gcc41。安装其他gcc的时候也是改名为gccXX(XX为版本)。这样的软链接是比较有好处的,感谢那些天才们!
探索
Hello World 这个让程序向外界说出第一句话的程序总是那么让人振奋。
#include <linux/init.h>
#inlcude <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, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
如果你以前没有太多的看过Linux的内核代码,或者对C语言不是非常了解,请不要对MODULE_LICENSE之类的感到奇怪。这个是宏的运用,在内核和驱动程序中你会看到满地都是这样难懂的东西。而我们现在还没有必要搞懂这到底是什么,到了后面我们再来探讨这些内容。
实践
我们先来运行这个东西把,makefile 的具体内容请参看书中的代码。
我们运行 #make
得到hello.o hello.ko 几个文件。
insmod 命令用于加载模块;rmmod 命令用于卸载模块。
装载: #insmod hello.ko 结果并没有象预期的那样输出Hello,world这句话。(我在path中加入了.路径如果没有加的话,需要使用 #insmod ./hello.ko)
再次运行#insmod hello.ko
得到:Error inserting ‘hello.ko’:-1 Invalid module format
很纳闷,第一次没有任何回显,而第二次出错!查看了书上的内容发现在某些系统中不会回显到终端,而是写入了/var/log/message 打开这个文件发现确实输入了这个文件。
尝试#rmmod hello
再次#insmod hello.ko 成功
模块的最大的好处就是你可以在使用它的时候动态装载它,如果不使用它你可以把它从内核中卸载掉。而对于调试来说这样也是很有好处的,可以最大程度上避免驱动程序引起系统的崩溃,同时也为调试带来一些方便。
预备知识
头文件 #include <linux/module.h> #include <linux/init.h> 这2个文件是每一个驱动模块所必须包含的。
一个为moudle的结构定义以及大量的符号函数,init.h为指定初始化和清除函数。
构造和清除:
看到这一段的时候,我很奇怪的反应出类的构造和析构函数。构造函数为类的初始化做了一些工作,而析构则清理类在运行过程中产生的数据(主要是释放)。当然这并不意味这一旦类的实例消失了,一切都消失的无影无踪。持久类就是个例子(关于持久类的知识请查看面向对象方面的书籍),对象的状态的改变会影响到存储介质。我们来看驱动模块其实很类似,也有持久的概念,还有全局的概念。
Static int __init initialization_function(void)
{
}
Module_init(initialization_function)
在上面的例子中 __init 并不是必须的,其作用只是个指示而已!当然对编译运行上也有些帮助,这个书上讲的比较明白。Static 也不是必须的,但容易造成重名,最好还是使用吧。
Static void __exit cleanup_function(void)
{
}
Module_exit(cleanup_function);
同样__exit 非必须。
模块中的事务及回滚
事务是原子的不可分割的若干操作的序列,在这里初始化函数我们可以认为是一个事务,一旦发生任何的错误,就需要回滚(roll back)。因为如果一个驱动程序需要在启动初始化的时候分配到4个设备,一旦一个失败对于整体来说都是不可用的。那么我们要做的就是回滚,并且恢复到启动函数执行前的状况。
int scull_init_module(void)
{
int result, i;
dev_t dev = 0;
/*
* Get a range of minor numbers to work with, asking for a dynamic
* major unless directed otherwise at load time.
*/
if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,
"scull");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}
/*
* allocate the devices -- we can't have them static, as the number
* can be specified at load time
*/
scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
if (!scull_devices) {
result = -ENOMEM;
goto fail; /* Make this more graceful */
}
memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));
/* Initialize each device. */
for (i = 0; i < scull_nr_devs; i++) {
scull_devices[i].quantum = scull_quantum;
scull_devices[i].qset = scull_qset;
init_MUTEX(&scull_devices[i].sem);
scull_setup_cdev(&scull_devices[i], i);
}
/* At this point call the init function for any friend device */