深入浅出Linux设备驱动编程
一.引言........................................................................................................... 错误!未定义书签。二.Linux内核模块ﻩ错误!未定义书签。
三.字符设备驱动程序...................................................................................... 错误!未定义书签。
四.设备驱动中的并发控制.............................................................................. 错误!未定义书签。五.设备的阻塞与非阻塞操作....................................................................... 错误!未定义书签。
六.设备驱动中的异步通知.............................................................................. 错误!未定义书签。七.设备驱动中的中断处理ﻩ错误!未定义书签。
八.定时器ﻩ错误!未定义书签。
九.内存与I/O操作ﻩ错误!未定义书签。
十.结构化设备驱动程序ﻩ错误!未定义书签。
十一.复杂设备驱动.......................................................................................... 错误!未定义书签。
一.引言
目前,Linux软件工程师大致可分为两个层次:
(1)Linux应用软件工程师(ApplicationSoftware Engineer):主要利用C库函数和Linux API进行应用软件的编写;ﻫﻫ(2)Linux固件工程师(FirmwareEngine er):主要进行Bootloader、Linux的移植及Linux设备驱动程序的设计。ﻫﻫ一般而言,固件工程师的要求要高于应用软件工程师的层次,而其中的Linux设备驱动编程又是Linux程序设计中比较复杂的部分,究其原因,主要包括如下几个方面:ﻫﻫ(1)设备驱动属于Linux 内核的部分,编写Linux设备驱动需要有一定的Linux操作系统内核基础;
(2)编写Linux设备驱动需要对硬件的原理有相当的了解,大多数情况下我们是针对一个特定的嵌入式硬件平台编写驱动的;
(3)Linux设备驱动中广泛涉及到多进程并发的同步、互斥等控制,容易出现bug;ﻫﻫ(4)由于属于内核的一部分,Linux设备驱动的调试也相当复杂。
目前,市面上的Linux设备驱动程序参考书籍非常稀缺,少有的经典是由Linux社区的三位领导者Jonathan Corbet、Alessandro Rubini、Greg Kroah-Hartman编写的《Linux Device Drivers》
(目前该书已经出版到第3版,中文译本由中国电力出版社出版)。该书将Linux设备驱动编写技术进行了较系统的展现,但是该书所列举实例的背景过于复杂,使得读者需要将过多的精力投放于对例子背景的理解上,很难完全集中精力于Linux 驱动程序本身。往往需要将此书翻来覆去地研读许多遍,才能有较深的体会。
ﻫ(《Linux Device Drivers》中英文版封面)
本文将仍然秉承《Linux Device Drivers》一书以实例为主的风格,但是实例的背景将非常简单,以求使读者能将集中精力于Linux设备驱动本身,理解Linux内核模块、Linux设备驱动的结构、Linux设备驱动中的并发控制等内容。另外,与《Linux Device Drivers》所不同的是,针对设备驱动的实例,本文还给出了用户态的程序来访问该设备,展现设备驱动的运行情况及用户态和内核态的交互。相信阅读完本文将为您领悟《Linux Dev ice Drivers》一书中的内容打下很好的基础。
ﻫ本文中的例程除引用的以外皆由笔者亲自调试通过,主要基于的内核版本为Linux2.4,例子要在其他内核上运行只需要做少量的修改。ﻫ构建本文例程运行平台的一个较好方法是:在Windows平台上安装VMWare虚拟机,并在VMWare虚拟机上安装RedHat。注意安装的过程中应该选中"开发工具"和"内核开发"二项(如果本文的例程要在特定的嵌入式系统中运行,还应安装相应的交叉编译器,并包含相应的Linux源代码),如下图:linux内核视频教程全套
二.Linux内核模块
Linux设备驱动属于内核的一部分,Linux内核的一个模块可以以两种方式被编译和加载:ﻫ
(1)直接编译进Linux内核,随同Linux启动时加载;ﻫﻫ(2)编译成一个可加载和删除的模块,使用insmod加载(modprobe和insmod命令类似,但依赖于相关的配置文件),rmmod删除。这种方式控制了内核的大小,而模块一旦被插入内核,它就和内核其他部分一样。ﻫﻫ下面我们给出一个内核模块的例子:
insmod的时候运行,后者在rmmod的时候运行。初始化与卸载函数必须在宏module_init和module_exit使用前定义,否则会出现编译错误。
程序中的MODULE_LICENSE("GPL")用于声明模块的许可证。
如果要把上述程序编译为一个运行时加载和删除的模块,则编译命令为:
ﻫE-DLINUX参数。-I选项跟着Linux内核源代码中Include目录的路径。ﻫ下列命令将可加载hello模块:
如果要将其直接编译入Linux内核,则需要将源代码文件拷贝入Linux内核源代码的相应路径里,并修改Makefile。ﻫ我们有必要补充一下Linux内核编程的一些基本知识:ﻫﻫ内存ﻫﻫ在Linux内核模式下,我们不能使用用户态的malloc()和free()函数申请和释放内存。进行内核编程时,最常用的内存申请
和释放函数为在include/linux/kernel.件中声明的kmalloc()和kfree(),其原型为:
kmalloc的priority参数通常设置为GFP_KERNEL,如果在中断服务程序里申请内存则要用GFP_ATOMIC参数,因为使用GFP_KERNEL参数可能会引起睡眠,不能用于非进程上下文中(在中断中是不允许睡眠的)。
由于内核态和用户态使用不同的内存定义,所以二者之间不能直接访问对方的内存。而应该使用Linux中的用户和内核态内存交互函数(这些函数在include/asm/uaccess.h中被声明):
copy_from_user、copy_to_user函数返回不能被复制的字节数,因此,如果完全复制成功,返回值为0。
ﻫinclude/asm/uaccess.h中定义的put_user和get_user用于内核空间和用户空间的单值交互(如char、int、long)。ﻫ
这里给出的仅仅是关于内核中内存管理的皮毛,关于Linux内存管理的更多细节知识,我们会在本文第9节《内存与I/O操作》进行更加深入地介绍。
输出
在内核编程中,我们不能使用用户态C库函数中的printf()函数输出信息,而只能使用printk()。但是,内核中printk()函数的设计目的并不是为了和用户交流,它实际上是内核的一种日志机制,用来记录下日志信息或者给出警告提示。
ﻫ每个printk都会有个优先级,内核一共有8个优先级,它们都有对应的宏定义。如果未指定优先级,内核会选择默认的优先级DEFAULT_MESSAGE_LOGLEVEL。如果优先级数字比int console_loglevel变量小的话,消息就会打印到控制台上。如果syslogd 和klogd守护进程在运行的话,则不管是否向控制台输出,消息都会被追加进/var/log/messages文件。klogd 只处理内核消息,syslogd 处理其他系统消息,比如应用程序。ﻫ模块参数ﻫ
2.4内核下,include/linux/module.h中定义的宏MODULE_PARM(var,type)用于向模块
传递命令行参数。var为接受参数值的变量名,type为采取如下格式的字符串[min[-max]]{b,h,i,l,s}。min及max用于表示当参数为数组类型时,允许输入的数组元素的个数范围;b:byte;h:short;i:int;l:long;s:string。
ﻫ在装载内核模块时,用户可以向模块传递一些参数:
insmod modname var=value
ﻫ如果用户未指定参数,var将使用模块内定义的缺省值。