深入浅出Linux 设备驱动编程.pdf
《深入浅出Linux 设备驱动编程.pdf》由会员分享,可在线阅读,更多相关《深入浅出Linux 设备驱动编程.pdf(53页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、 目目 录录 1.引言.1 2.Linux 内核模块.2 3.字符设备驱动程序.4 4.设备驱动中的并发控制.10 5.设备的阻塞与非阻塞操作.16 6.设备驱动中的异步通知.25 7.设备驱动中的中断处理.26 8.定时器.30 9.内存与 I/O 操作.32 10.结构化设备驱动程序.39 11.复杂设备驱动.40 12.总结.52 1深入浅出深入浅出 Linux 设备驱动编程设备驱动编程 宋宝华 1.引言引言 目前,Linux 软件工程师大致可分为两个层次:(1)Linux 应用软件工程师(Application Software Engineer):主要利用 C 库函数和 Linux
2、API 进行应用软件的编写;(2)Linux 固件工程师(Firmware Engineer):主要进行 Bootloader、Linux 的移植及Linux 设备驱动程序的设计。一般而言,固件工程师的要求要高于应用软件工程师的层次,而其中的 Linux 设备驱动编程又是 Linux 程序设计中比较复杂的部分,究其原因,主要包括如下几个方面:(1)设备驱动属于 Linux 内核的部分,编写 Linux 设备驱动需要有一定的 Linux 操作系统内核基础;(2)编写 Linux 设备驱动需要对硬件的原理有相当的了解,大多数情况下我们是针对一个特定的嵌入式硬件平台编写驱动的;(3)Linux 设备
3、驱动中广泛涉及到多进程并发的同步、互斥等控制,容易出现 bug;(4)由于属于内核的一部分,Linux 设备驱动的调试也相当复杂。目前,市面上的 Linux 设备驱动程序参考书籍非常稀缺,少有的经典是由 Linux 社区的三位领导者 Jonathan Corbet、Alessandro Rubini、Greg Kroah-Hartman 编写的Linux Device Drivers(目前该书已经出版到第 3 版,中文译本由中国电力出版社出版)。该书将 Linux 设备驱动编写技术进行了较系统的展现,但是该书所列举实例的背景过于复杂,使得读者需要将过多的精力投放于对例子背景的理解上,很难完全集
4、中精力于 Linux 驱动程序本身。往往需要将此书翻来覆去地研读许多遍,才能有较深的体会。(Linux Device Drivers中英文版封面)本文将仍然秉承Linux Device Drivers一书以实例为主的风格,但是实例的背景将非常简单,以求使读者能将集中精力于 Linux 设备驱动本身,理解 Linux 内核模块、Linux 设备驱动的结构、Linux 设备驱动中的并发控制等内容。另外,与Linux Device Drivers所不同的是,针对设备驱动的实例,本文还给出了用户态的程序来访问该设备,展现设备驱动的运行情况及用户态和内核态的交互。相信阅读完本文将为您领悟Linux De
5、vice Drivers一书中的内容打下很好的基础。本文中的例程除引用的以外皆由笔者亲自调试通过,主要基于的内核版本为 Linux 2.4,例子要在其他内核上运行只需要做少量的修改。构建本文例程运行平台的一个较好方法是:在 Windows 平台上安装 VMWare 虚拟机,并在 VMWare 虚拟机上安装 Red Hat。注意安装的过程中应该选中“开发工具”和“内核开发”二项(如果本文的例程要在特定的嵌入式系统中运行,还应安装相应的交叉编译器,并 2包含相应的 Linux 源代码),如下图:2.Linux 内核模块内核模块 Linux 设备驱动属于内核的一部分,Linux 内核的一个模块可以以
6、两种方式被编译和加载:(1)直接编译进 Linux 内核,随同 Linux 启动时加载;(2)编译成一个可加载和删除的模块,使用 insmod 加载(modprobe 和 insmod 命令类似,但依赖于相关的配置文件),rmmod 删除。这种方式控制了内核的大小,而模块一旦被插入内核,它就和内核其他部分一样。下面我们给出一个内核模块的例子:#include /所有模块都需要的头文件#include /init&exit 相关宏 MODULE_LICENSE(GPL);static int _init hello_init(void)printk(Hello module initn);ret
7、urn 0;static void _exit hello_exit(void)printk(Hello module exitn);3 module_init(hello_init);module_exit(hello_exit);分析上述程序,发现一个 Linux 内核模块需包含模块初始化和模块卸载函数,前者在insmod 的时候运行,后者在 rmmod 的时候运行。初始化与卸载函数必须在宏 module_init和 module_exit 使用前定义,否则会出现编译错误。程序中的 MODULE_LICENSE(GPL)用于声明模块的许可证。如果要把上述程序编译为一个运行时加载和删除的模块
8、,则编译命令为:gcc D_KERNEL_-DMODULE DLINUX I/usr/local/src/linux2.4/include-c o hello.o hello.c 由此可见,Linux 内核模块的编译需要给 gcc 指示D_KERNEL_-DMODULE DLINUX 参数。-I 选项跟着 Linux 内核源代码中 Include 目录的路径。下列命令将可加载 hello 模块:insmod./hello.o 下列命令完成相反过程:rmmod hello 如果要将其直接编译入 Linux 内核,则需要将源代码文件拷贝入 Linux 内核源代码的相应路径里,并修改 Makefil
9、e。我们有必要补充一下 Linux 内核编程的一些基本知识:内存内存 在 Linux 内核模式下,我们不能使用用户态的 malloc()和 free()函数申请和释放内存。进行内核编程时,最常用的内存申请和释放函数为在 include/linux/kernel.h 文件中声明的kmalloc()和 kfree(),其原型为:void*kmalloc(unsigned int len,int priority);void kfree(void*_ptr);kmalloc 的 priority 参数通常设置为 GFP_KERNEL,如果在中断服务程序里申请内存则要用 GFP_ATOMIC 参数,因
10、为使用 GFP_KERNEL 参数可能会引起睡眠,不能用于非进程上下文中(在中断中是不允许睡眠的)。由于内核态和用户态使用不同的内存定义,所以二者之间不能直接访问对方的内存。而应该使用 Linux 中的用户和内核态内存交互函数(这些函数在 include/asm/uaccess.h 中被声明):unsigned long copy_from_user(void*to,const void*from,unsigned long n);unsigned long copy_to_user(void*to,void*from,unsigned long len);copy_from_user、cop
11、y_to_user 函数返回不能被复制的字节数,因此,如果完全复制成功,返回值为 0。include/asm/uaccess.h中定义的put_user和get_user用于内核空间和用户空间的单值交互(如 char、int、long)。这里给出的仅仅是关于内核中内存管理的皮毛,关于 Linux 内存管理的更多细节知识,我们会在本文第 9 节内存与 I/O 操作进行更加深入地介绍。输出输出 在内核编程中,我们不能使用用户态 C 库函数中的 printf()函数输出信息,而只能使用printk()。但是,内核中 printk()函数的设计目的并不是为了和用户交流,它实际上是内核的一种日志机制,用
12、来记录下日志信息或者给出警告提示。4每个 printk 都会有个优先级,内核一共有 8 个优先级,它们都有对应的宏定义。如果未指定优先级,内核会选择默认的优先级 DEFAULT_MESSAGE_LOGLEVEL。如果优先级数字比 int console_loglevel 变量小的话,消息就会打印到控制台上。如果 syslogd 和 klogd 守护进程在运行的话,则不管是否向控制台输出,消息都会被追加进/var/log/messages 文件。klogd 只处理内核消息,syslogd 处理其他系统消息,比如应用程序。模块参数模块参数 2.4 内核下,include/linux/module.
13、h 中定义的宏 MODULE_PARM(var,type)用于向模块传递命令行参数。var 为接受参数值的变量名,type 为采取如下格式的字符串min-maxb,h,i,l,s。min 及 max 用于表示当参数为数组类型时,允许输入的数组元素的个数范围;b:byte;h:short;i:int;l:long;s:string。在装载内核模块时,用户可以向模块传递一些参数:insmod modname var=value 如果用户未指定参数,var 将使用模块内定义的缺省值。3.字符设备驱动程序字符设备驱动程序 Linux 下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过这些函数使
14、得Windows 的设备操作犹如文件一般。在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作,如 open()、close()、read()、write()等。Linux 主要将设备分为二类:字符设备和块设备。字符设备是指设备发送和接收数据以字符的形式进行;而块设备则以整个数据缓冲区的形式进行。字符设备的驱动相对比较简单。下面我们来假设一个非常简单的虚拟字符设备:这个设备中只有一个 4 个字节的全局变量 int global_var,而这个设备的名字叫做“gobalvar”。对“gobalvar”设备的读写等操作即是对其中全局变量 global_var
15、的操作。驱动程序是内核的一部分,因此我们需要给其添加模块初始化函数,该函数用来完成对所控设备的初始化工作,并调用 register_chrdev()函数注册字符设备:static int _init gobalvar_init(void)if(register_chrdev(MAJOR_NUM,gobalvar,&gobalvar_fops)/注册失败 else /注册成功 其中,register_chrdev 函数中的参数 MAJOR_NUM 为主设备号,“gobalvar”为设备名,gobalvar_fops 为包含基本函数入口点的结构体,类型为 file_operations。当 gob
16、alvar 模块被加载时,gobalvar_init 被执行,它将调用内核函数 register_chrdev,把驱动程序的基本入口点指针存放在内核的字符设备地址表中,在用户进程对该设备执行系统调用时提供入口地址。与模块初始化函数对应的就是模块卸载函数,需要调用 register_chrdev()的“反函数”unregister_chrdev():static void _exit gobalvar_exit(void)5 if(unregister_chrdev(MAJOR_NUM,gobalvar)/卸载失败 else /卸载成功 随着内核不断增加新的功能,file_operations
17、结构体已逐渐变得越来越大,但是大多数的驱动程序只是利用了其中的一部分。对于字符设备来说,要提供的主要入口有:open()、release()、read()、write()、ioctl()、llseek()、poll()等。open()函数函数 对设备特殊文件进行 open()系统调用时,将调用驱动程序的 open()函数:int(*open)(struct inode*,struct file*);其中参数 inode 为设备特殊文件的 inode(索引结点)结构的指针,参数 file 是指向这一设备的文件结构的指针。open()的主要任务是确定硬件处在就绪状态、验证次设备号的合法性(次设备号
18、可以用 MINOR(inode-i-rdev)取得)、控制使用设备的进程数、根据执行情况返回状态码(0 表示成功,负数表示存在错误)等;release()函数函数 当最后一个打开设备的用户进程执行 close()系统调用时,内核将调用驱动程序的 release()函数:void(*release)(struct inode*,struct file*);release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。read()函数函数 当对设备特殊文件进行 read()系统调用时,将调用驱动程序 read()函数:ssize_t(*read)(struct
19、file*,char*,size_t,loff_t*);用来从设备中读取数据。当该函数指针被赋为 NULL 值时,将导致 read 系统调用出错并返回-EINVAL(“Invalid argument,非法参数”)。函数返回非负值表示成功读取的字节数(返回值为“signed size”数据类型,通常就是目标平台上的固有整数类型)。globalvar_read 函数中内核空间与用户空间的内存交互需要借助第 2 节所介绍的函数:static ssize_t globalvar_read(struct file*filp,char*buf,size_t len,loff_t*off)copy_to_
20、user(buf,&global_var,sizeof(int);write()函数函数 当设备特殊文件进行 write()系统调用时,将调用驱动程序的 write()函数:ssize_t(*write)(struct file*,const char*,size_t,loff_t*);向设备发送数据。如果没有这个函数,write 系统调用会向调用程序返回一个-EINVAL。如果返回值非负,则表示成功写入的字节数。globalvar_write 函数中内核空间与用户空间的内存交互需要借助第 2 节所介绍的函数:static ssize_t globalvar_write(struct file
21、*filp,const char*buf,size_t len,loff_t *off)6 copy_from_user(&global_var,buf,sizeof(int);ioctl()函数函数 该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:int(*ioctl)(struct inode*,struct file*,unsigned int,unsigned long);unsigned int 参数为设备驱动程序要执行的命令的代码,由用户自定义,unsigned long参数为相应的命令提供参数,类型可以是整型、指针等。如果设备不提供 ioct
22、l 入口点,则对于任何内核未预先定义的请求,ioctl 系统调用将返回错误(-ENOTTY,“No such ioctl fordevice,该设备无此 ioctl 命令”)。如果该设备方法返回一个非负值,那么该值会被返回给调用程序以表示调用成功。llseek()函数函数 该函数用来修改文件的当前读写位置,并将新位置作为(正的)返回值返回,原型为:loff_t(*llseek)(struct file*,loff_t,int);poll()函数函数 poll 方法是 poll 和 select 这两个系统调用的后端实现,用来查询设备是否可读或可写,或是否处于某种特殊状态,原型为:unsigne
23、d int(*poll)(struct file*,struct poll_table_struct*);我们将在“设备的阻塞与非阻塞操作”一节对该函数进行更深入的介绍。设备“gobalvar”的驱动程序的这些函数应分别命名为 gobalvar_open、gobalvar_ release、gobalvar_read、gobalvar_write、gobalvar_ioctl,因此设备“gobalvar”的基本入口点结构变量gobalvar_fops 赋值如下:struct file_operations gobalvar_fops=read:gobalvar_read,write:gobal
24、var_write,;上述代码中对 gobalvar_fops 的初始化方法并不是标准 C 所支持的,属于 GNU 扩展语法。完整的 globalvar.c 文件源代码如下:#include#include#include#include MODULE_LICENSE(GPL);#define MAJOR_NUM 254 /主设备号 static ssize_t globalvar_read(struct file*,char*,size_t,loff_t*);static ssize_t globalvar_write(struct file*,const char*,size_t,loff
25、_t*);/初始化字符设备驱动的 file_operations 结构体 struct file_operations globalvar_fops=7 read:globalvar_read,write:globalvar_write,;static int global_var=0;/“globalvar”设备的全局变量 static int _init globalvar_init(void)int ret;/注册设备驱动 ret=register_chrdev(MAJOR_NUM,globalvar,&globalvar_fops);if(ret)printk(globalvar re
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 深入浅出Linux 设备驱动编程 深入浅出 Linux 设备 驱动 编程
限制150内