Linux设备驱动

这两天在看LDD2,为了让我那个可怜的Zaurus能用上该死的CF8385的无线网卡,我必须要编译它的驱动,网上找到的驱动是2.4.22的,在2.4.20上编译出了错误,只好硬着头皮找来linux驱动的资料看。唔,LDD里的英文真不错,通俗易懂,童叟无欺,边看边翻译,竟也攒了不少。今天就先贴出来些吧。


内核模块和应用程序



在我们继续深入之前,有必要强调一下一个内核模块和一个应用程序之间的区别。



与应用程序从头到尾完成一个完整的任务不同,模块将自己登录到内核,为响应将来的请求做准备,之后它的“main”函数就会终止。换句话说,函数init_module(模块的入口)为之后到来的对模块中的其他函数的调用作准备。就好像模块在说:“我在这里,我能为你做这些事情”。模块的另一个入口,cleanup_module,会在模块被卸载的时候被调用,它会告诉内核,“我不再在那里了,不要再让我做什么了。”卸载模块的能力是所有模块化的特性中你最应该欣赏的一个。因为它帮助你节省了开发时间,你可以持续的改进并测试你的驱动而不需要不断的重复关机/重启的过程。



作为一个程序员,你知道一个应用程序可以调用它没有定义过的函数:在链接阶段可以使用适当的函数库解析外部引用。printf就是在libc中定义的可调用函数之一。而另一方面,模块仅仅被链接到内核上,没有别的函数库可以链接,所以它仅仅可以调用的内核提供的函数。例如,之前在hello.c中使用的printk函数是printf的另一个版本,它在内核中定义,并向模块公开。他的行为和原始版本相似,但有一些细小的差别,最主要的一个是缺少对浮点数的支持。



图2-1显示了模块如何使用函数调用和函数指针来向运行中的内核加入新的功能。



因为不会有程序库连接到模块上,所以源文件绝对不应包含通常的头文件,在内核模块中只能调用内核的函数。所有和内核有关的东西都被声明在内核源代码的include/linux和include/asm的头文件中(通常保存在/usr/src/linux)。早期的发行版(基于libc5或更早的版本)多采用符号连接的方式将头文件从/usr/include/linux 和 /usr/include/asm链接到实际安装的内核源代码中。所以你的libc包括树结构会指向实际安装的内核源代码的头文件。这些符号连接让用户空间的应用程序可以方便的包含内核的头文件--它们偶尔需要这样做。



虽然用户空间的头文件现在被从内核空间的头文件分离了出来,有时应用程序仍然需要包含内核头文件--例如在使用早期的程序库,或者从用户空间的头文件无法获得所需信息的时候。然而,在内核的头文件中有很多声明是仅仅关于内核本身,并且不希望为用户应用程序所见的,这些声明都被#ifdef __KERNEL__块保护起来了。这就是为什么你的驱动,以及其他的内核代码,在编译的时候要加上__KERNEL__的预处理定义的原因。



各个内核头文件的作用会随着本书的展开而在需要时介绍。



在任何一个大型软件系统(例如内核系统)中工作的开发者,一定会知道并尽力避免发生"命名空间污染"的情况。命名空间污染发生在系统中存在大量函数名和全局变量名意义不明确而无法轻易识别的时候。在这样的系统中开发的程序员要花费大量的精力记住已经保留的名字,并为新的符号找到一个新的唯一的名字。命名冲突会造成各种错误,模块加载失败或其他奇怪的错误,它们也许只会发生在其他用户使用不同的配置选项构建了内核的时候。



内核代码不应仅仅因为一个模块被连接进来中就产生这样的错误。杜绝命名空间污染的最好做法就是将你所有的符号声明为static并为全局符号使用唯一的前缀。另外还要记得,作为一个模块的开发者,你可以控制你的符号的可见性,就像本章稍后会讲述的“内核符号表”所做的那样。



为模块里的为私有符号选择一个前缀也是一个好的作法。这回简化除错的过程。在测试你的驱动的时候,你可以暴露所有的符号而不污染你的命名空间。在内核中使用的前缀习惯上都使用小写字母,我们会遵守相同的作法。



在内核编程和应用编程之间最后一个不同与各自的环境如何处理错误有关。一个分割错误在应用开发的时候是无害的,出错器总是可以在源代码中跟踪并找到这样的错误。而一个内核错误则是致命的,至少对于现在的进程而言,如果不是对于整个系统而言的话。我们将会在第四章看到如何跟踪内核错误。



コメント

人気の投稿