编写设备驱动程序的注意事项
应用程序开发与驱动程序开发的差异
在Linux上的程序开发一般分为两种,一种是内核及驱动程序开发,另一种是应用程序开发。这两种开发种类对应Linux的两种状态,分别是内核态和用户态。内核态用来管理用户态的程序,完成用户态请求的工作;用户态处理上层的软件工作。驱动程序与底层的硬件交互,所以工作在内核态。
大多数程序员致力于应用程序的开发,少数程序员则致力于内核及驱动程序的开发。相对于应用程序的开发,内核及驱动程序的开发有很大的不同。最重要的差异包括以下几点:
- 内核及驱动程序开发时不能访问C库,因为C库是使用内核中的系统调用来实现的,而且是在用户空间实现的。
- 内核及驱动程序开发时必须使用GNU C,因为Linux操作系统从一开始就使用的是GNU C,虽然也可以使用其他的编译工具,但是需要对以前的代码做大量的修改。
- 内核支持异步终端、抢占和SMP,,因此内核及驱动程序开发时必须时刻注意同步和并发。
- 内核只有一个很小的定长堆栈。
- 内核及驱动程序开发时缺乏像用户空间那样的内存保护机制。
- 内核及驱动程序开发时浮点数很难使用,应该使用整型数。
- 内核及驱动程序开发要考虑可移植性,因为对于不同的平台,驱动程序是不兼容的。
GUN C开发驱动程序
GUN C语言最早起源于一个GUN计划,GUN的意思是“GUN is not UNIX”。GUN计划开始于1984年,这个计划的目的是开发一个类似UNIX并且软件自由的完整操作系统。这个计划一直在进行,直到Linus开发Linux操作系统时,GNU计划已经开发出来了很多高质量的自由软件,其中就包括著名的GCC编译器,GCC编译器能够编译GUN C语言。Linus考虑到GUN计划的自由和免费,所以选择了GCC编译器来编写内核代码,之后的很多开发者也使用这个编译器,所以直到现在,驱动开发人员也使用GUN C语言来开发驱动程序。
不能使用C库开发驱动程序
与用户空间的应用程序不同,内核不能调用标准的C函数库,主要的原因在于对于内核来说完整的C库太大了。一个编译的内核大小可以是1MB左右,而一个标准的C语言库大小可能操作5MB。这对于存储容量较小的嵌入式设备来说,是不实用的。缺少标志C语言库,并不是说驱动程序就只能做很好的事情了。
大部分常用的C库函数在内核中都已经实现了。比如操作字符串的函数组就位于内核文件lib/string.c中。只要包含<linux/string.h>,就可以使用它们。又如内存分配的函数也已经包含在include/linux/slab_def.h中实现了。注意:内核程序中包含的头文件是指内核代码树中的内核头文件,不是指开发应用程序时的外部头文件。在内核中实现的库函数中的打印函数printk(),它是C库函数printf()的内核版本。printk()函数和printf()函数有基本相同的用法和功能。
没有内存保护机制
当一个用户应用程序由于编程错误,试图访问一个非法的内存空间,那么操作系统内核会结束这个进程,并返回错误码。应用程序可以在操作系统内核的帮助下恢复过来,而且应用程序并不会对操作系统内核有太大的影响。但是如果操作系统内核访问了一个非法的内存,那么就有可能破坏内核的代码或者数据。这将导致内核处于未知的状态,内核会通过oops错误给用户一些提示,但是这些提示都是不支持、难以分析的。
在内核编程中,不应该访问非法内存,特别是空指针,否则,内核会忽然死掉,没有任何机会给用户提示。对于不好的驱动程序,引起系统崩溃是很常见的事情,所以对于驱动开发人员来说,应该非常重视对内存的正确访问。一个好的建议是,当申请内存后,应该对返回的地址进行检测。
小内核栈
用户空间的程序可以从栈上分配大量的空间存放变量,甚至用栈存放巨大的数据结构或者数组都没问题。之所以能这样做是因为应用程序是非常驻内存的,它们可以动态地申请和释放所有可用的内存空间。内核要求使用固定常驻的内存空间,因此要求尽量少地占用常驻内存,而尽量多地留出内存提供给用户程序使用。因此内核栈的长度是固定大小的,不可动态增长的32位机的内核栈是8KB;64位机的内核栈是16KB。
由于内核栈比较小,所以编写程序时,应该充分考虑小内核栈问题。尽量不要使用递归调用,在应用程序中,递归调用4000多次就有可能溢出,在内核中,递归调用的次数非常少,几乎不能完成程序的功能。另外按使用完内存空间后,应该尽快地释放内存,以防止资源泄漏,引起内核崩溃。
重视可移植性
对于用户空间的应用程序来说,可移植性一直是一个重要的问题。一般可移植性通过两种方式来实现。一种方式是定义一套可移植的API,然后对这套API在这两个需要移植的平台上分别实现。应用程序开发人员,只要使用这套可移植的API,就可以写出可移植的程序。在嵌入式领域,比较常见的API套件是QT。另一种方式是使用类似Java、Actionscript等可移植到很多操作系统上的语言。这些语言一般通过虚拟机执行,所以可以移植到很多平台上。
对于驱动程序来说,可移植性需要注意以下几个问题:
- 考虑字节顺序,一些设备使用大端字节序,一些设备使用小端字节序。Linux内核提供了大小端字节节序转换的函数。
#define cpu_to_le16(v16) (v16) #define cpu_to_le32(v32) (v32) #define cpu_to_le64(v64) (v64) #define le16_to_cpu(v16) (v16) #define le32_to_cpu(v32) (v32) #define le64_to_cpu(v64) (v64)
- 即使是同一种设备的驱动程序,如果使用的芯片不同,也应该写不同的驱动程序,但是应该给用户提供一个统一的编程接口。
- 尽量使用宏代替设备端口的物理地址,并且可以使用ifdefine宏确定版本等信息。
- 针对不同的处理器,应该使用相关处理器的函数。