第六章 第一个Linux驱动程序:统计单词个数
一、Linux驱动到底是个什么东西:
1、 实际上Linux驱动和普通的Linux API没有本质的区别,只是使用Linux驱动的方式与使用Linux API的方式不同而已;
2、 Linux系统将每一个驱动都映射成一个文件。这些文件称为设备文件或驱动文件,都保存在/dev目录中。这种设计理念使得与Linux驱动进行交互就像与普通文件进行交互一样容易,比访问Linux API也容易。由于大多数Linux驱动都有与其对应的设备文件,因此与Linux驱动交换数据就变成了与设备文件交换数据。
3、 要编写Linux驱动程序还需要更高级的功能,要实现这一过程就需要Linux驱动可以响应应用程序传递过来的数据。这就是Linux驱动的事件,虽然在C语言中没有事件的概念,但有与事件类似的概念,即回调函数。因此,编写Linux驱动最重要的一步就是编写回调函数,否则与设备文件交互的数据将无法得到处理。
4、 应用软件、设备文件、驱动程序、硬件之间的关系:
二、编写Linux驱动程序的步骤
第1步:建立Linux驱动骨架(装载和卸载Linux驱动)
任何类型的程序都有一个基本的结构。Linux驱动程序也不例外。
Linux内核在使用驱动时首先需要装载驱动。在装载过程中需要进行一些初始化工作,例如,建立设备文件,分配内存地址空间等。当Linux系统退出时需要卸载Linux驱动,在卸载过程中要释放由Linux驱动占用的资源,例如,删除设备文件、释放内存地址空间等。
在Linux驱动程序中需要提供两个函数来分别处理驱动初始化和退出的工作。这两个函数分别用module_init()和module_exit()宏指定。Linux驱动程序一般都需要指定这两个函数,因此包含着两个函数的两个宏的C程序文件也可看作是Linux驱动的骨架。
第2步:注册和注销设备文件
任何一个Linux驱动都需要有一个设备文件,否则应用程序将无法与驱动程序交互。
建立设备文件的作用一般在第1步编写处理Linux初始化工作的函数中完成。删除设备文件一般在第1步编写的处理Linux退出工作的函数中完成,可以分别使用misc_register和misc_deregister函数创建和移除设备文件。
第3步:指定与驱动相关的信息
驱动程序是自描述的。可以通过modinfo命令获取驱动程序的作者姓名、使用的开源协议、别名、驱动描述等信息。这些信息都需要在驱动源代码中指定。
通过MODULE_AUTHOR、MODULE_LICENSE、MODULE_ALIAS、MODULE_DESCRIPTION等宏可以指定与驱动相关的信息。
第4步:指定回调函数
Linux驱动包含了多种动作,也可称为事件。例如,向设备文件写入数据时会触发“写”时间,Linux系统会调用对应驱动程序的write回调函数,从设备文件读数据时会触发“读”时间,Linux系统会调用对应驱动程序的read回调函数。
一个驱动程序并不一定要指定所有的回调函数。回调函数会通过相关机制进行注册。例如,与设备文件相关的回调函数会通过misc_register函数进行注册。
第5步:编写业务逻辑
这一步是Linux驱动的核心部分。光有骨架和回调函数的Linux驱动没有任何意义的。任何一个完整的Linux驱动都会做一些与其功能相关的工作,如打印机驱动会向打印机发送打印指令。COM驱动会根据传输数率进行数据交互。具体的业务逻辑与驱动的功能有关。
业务逻辑可能由多个函数、多个文件甚至是多个Linux驱动模块组成。根据实际情况而定。
第6步:编写Makefile文件
Linux内核源代码的编译规则时通过Makefile文件定义的。因此编写一个新的Linux驱动程序必须要有一个Makefile文件。
第7步:编译Linux驱动程序
Linux驱动程序可以直接编译进内核,也可以作为模块单独编译。
第8步:安装和卸载Linux驱动
如果将Linux驱动编译进内核,只要Linux使用该内核,驱动程序就会自动装载。
如果Linux驱动程序以模块单独存在,需要使用insmod或modprobe命令装载Linux驱动模块,使用rmmod命令卸载Linux驱动模块。
三、第一个Linux驱动:统计单词个数
1、 编写Linux驱动程序前的准备工作
(1) 首先创建存放Linux驱动程序的目录
命令:mkdir –p /root/drivers/ch06/wprd_count
(2) 建立驱动源代码文件
命令:echo ‘ ’ > word_count.c
(3) 编写Makefile文件
命令:echo ‘obj-m := word_count.o’ > Makefile
obj-m表示将Linux驱动作为模块(.ko文件)编译,obj-y表示将Linux驱动编译进Linux内核;
obj-m或obj-y需要使用”:=”赋值。如果obj-m或obj-y的值为word_count.o,表示make命令会把Linux驱动源代码目录中的word_count.c或word_count.s文件编译成word_count.o文件。如果使用obj-m,word_count.o会被连接进word_count.ko文件,然后使用insmod或modprobe命令装载word_count.ko;如果使用obj_y,word_count.o会被连接进built_in.o文件,最终会被连接进内核。其中built_in.o是连接同一类程序的.o文件生成的中间目标文件。例如,所有的字符设备驱动程序会最终生成一个built_in.o文件。
如果Linux驱动程序依赖其他程序,如process.c、data.c,需要按如下方式编写Makefile文件:
obj-m := word_count.o
word_count-y := process.o data.o
2、 编写Linux驱动程序框架
骨架部分主要是Linux驱动的初始化和退出函数。
代码:
printk()函数用于输出日志信息,与printf()用法类似。为什么不用printf()?这里涉及Linux内核程序可以调用什么,不可以调用什么的问题。Linux系统将内存分为了用户空间和内核空间,这两个空间的程序不能直接访问。printf()运行在用户空间,printk()运行在内核空间。因此,属于内核程序的Linux驱动是不能直接访问printf()的,就算包含了stdio.h头文件,在编译Linux驱动程序时也会抛出stdio.h文件没找到的错误。
运行在用户空间的程序也不能直接调用printk()。
是不是用户空间和内核空间的程序之间就无法交互呢?答案是否定的,否则这两块内存就成了孤岛。运行在这两块内存中的程序之间交互的方法很多。其中,设备文件就是一种主要的交互方式。如果用户空间的程序要访问内核空间,只要做一个可以访问内核空间的驱动程序,然后用户空间的程序通过设备文件与驱动程序进行交互即可。
Linux驱动程序无法直接访问运行在用户空间的程序,很多功能就得自己实现了。例如,在C语言中常使用malloc()动态分配内存空间,该函数在Linux驱动程序中是无法使用的。那么,如何在Linux驱动程序中动态分配内存空间呢?解决类似的问题很简单。既然Linux驱动无法直接调用运行在用户空间的函数,那么,在Linux内核中就必须要提供替代品。在<Linux内核源代码>/include目录中包含了大量的C语言头文件,这些头文件中定义的函数、宏等资源就是运行在用户空间的程序的替代品。运行在用户空间的函数库对应的头文件在/usr/include目录中。
编译Linux驱动程序源代码:
make –C /usr/src/linux-headers-3.0.0-15-generic M=/root/driver/ch06/word_count
测试Linux驱动时,不一定在Android设备上完成。因为Android系统和Ubuntu Linux及其他的Linux发行版本都是基于Linux内核的。大多数Linux驱动程序可以在Ubuntu Linux或其他Linux发行版上测试完再重新用交叉编译器编译成基于ARM架构的目标文件,然后再安装到Android上即可正常运行。
由于编译Linux内核源代码需要使用Linux内核的头文件。为了在Ubuntu Linux上测试驱动程序,需要使用-C命令行参数指定Linux内核头文件的目录(/usr/src/linux-headers-3.0.0-15-generic)。其中linux-headers-3.0.0-15-generic目录是Linux内核源代码目录,在该目录中只有include子目录有实际的头文件,其他目录只有Makefile和其他一些配置文件,并不包含Linux内核源代码。该目录就是为了开发当前Linux内核版本的驱动及其他内核程序而提供的(因为在编译Linux驱动时生成的目标文件只需要头文件,在进行目标文件链接时只要有相关的目标文件即可,并不需要源代码文件)。如果以模块方式编译Linux驱动程序,需要使用M指定驱动程序所在的目录(M=/root/drivers/ch06/word_count)。
驱动相关常用命令:
(1) 安装Linux驱动
insmod word_count.ko
(2) 查看word_count是否成功安装
lsmod | grep word_count
(3) 卸载Linux驱动
rmmod word_count
(4) 查看由Linux驱动输出的日志信息
dmesg | grep word_count | tail –n 2
dmesg实际上是从/var/log/messages(Ubuntu Linux10.04)或/var/log/syslog(Ubuntu Linux11.10)文件中读取的日志信息,因此也可以使用下面命令获取由Linux驱动输出的日志信息。
# cat /var/log/syslog | grep word_count | tail –n 2
3、 指定与驱动相关的信息
一个完整的Linux驱动程序都会指定这些与驱动相关的信息。
模块作者:MODULE_AUTHOR()
模块描述:MODULE_DESCRIPTION()
模块别名:MODULE_ALIAS()
开源协议:MODULE_LICENSE()
查看驱动信息:modinfo word_count.ko
depends表示当前驱动模块的依赖
vermagic表示当前Linux驱动模块在哪个Linux内核版本下编译的
4、 注册和注销设备文件
设备文件与普通文件不同,不能使用IO函数建立,需要使用misc_register()建立设备文件,使用misc_deregister()注销(移出)设备文件。
一般需要在初始化Linux驱动时建立设备文件,在卸载Linux驱动时删除设备文件。而且设备文件还需要一个结构体(miscdevice)来描述与其相关的信息。
miscdevice结构体中有一个重要的额成员变量fops,用于描述设备文件在各种可触发事件的函数指针。该成员变量的数据类型也是一个结构体file_operations。
设备文件由主设备号和次设备号描述,使用misc_register()只能设置次设备号。主设备号统一设为10。主设备号为10的设备是Linux系统中拥有共同特性的简单字符设备。这类设备称为misc设备。
miscdevice.name变量的值就是设备文件的名称。
虽然file_operations结构体中定义了多个回调函数指针变量,但是如果不需要的话,只需要初始化file_operations.owner变量。如果该变量的值为module结构体,表示file_operations可被应用在这些由module指定的驱动模块中。如果owner变量的值为THIS_MODULE,表示file_operations只应用于当前驱动模块。
如果成功注册了设备文件,misc_register()返回非0的整数,如果注册设备文件失败,返回0。
所有函数和变量都声明成了static。这是因为在C语言中,用static声明函数,变量等资源,系统会将这些函数和变量单独放在内存的某一个区域,直到程序完全退出,否则这些资源不会被释放。Linux驱动一旦装载,除非手动卸载或关机,驱动会一直驻留内存,因此这些函数和变量资源会一直在内存中。也就是说多次调用这些资源不用在进行压栈、出栈操作了,有利于提高驱动的运行效率。
显示当前系统中有哪些主设备以及主设备号:
cat /proc/devices
5、 指定回调函数
用户空间的应用程序和内核空间的驱动程序进行交互才有意义。最常用的交互方式是读写设备文件。