Android本质上是基于Linux内核的系统,也就是说Android就是一种Linux操作系统。只不过大多数时候都会运行在ARM架构的设备上,例如,Android手机、平板等。Android驱动实际上就是Linux驱动,只是这里使用Android深度探索(卷1):安装C/C++交叉编译环境
介绍的交叉编译器将Linux驱动编译成了ARM架构的,所以驱动可以安装在Android模拟器、Android手机(需要root)或平板上(这些设备都要使用给予ARM架构的CPU),当然,使用传统的GCC也可以编译成X86架构的驱动(并不需要修改代码),这样也可以在Ubuntu
Linux上安装Linux驱动。
本文及后面几篇文章主要介绍如何利用Android模拟器和S3C6410开发板开发给予ARM架构的Linux驱动,当然,测试的环境是Android,而不是我们通常使用的Ubuntu
Linux等X86架构的系统。最后会介绍通过多种方式测试这个驱动,测试方法包括命令行、NDK、Android程序(Java代码)等,当然,在最最后还会介绍如果将驱动嵌入到LInux内核中,这样Android在启动是就自动拥有了这个驱动。
想学习Android底层开发的童鞋可以通过本文完全掌握开发基于Android的LInux驱动的完整步骤。在《Android深度探索(卷1):HAL与驱动开发》随书光盘上有完整的实验环境(VMWare
Ubuntu Linux12.04LTS),如果嫌自己配置麻烦,可以从光盘中复制该虚拟环境,虚拟文件太大(3.6G),传不上去,只能发文章了!
一、Linux驱动到底是个什么东西
对于从未接触过驱动开发的程序员可能会感觉Linux驱动很神秘。感觉开发起来会很复杂。其实这完全是误解。实际上Linux驱动和普通的LinuxAPI没有本质的区别。只是使用Linux驱动的方式与使用Linux
API的方式不同而已
在学习Linux驱动之前我们先来介绍一下Linux驱动的工作方式。如果读者以前接触过Windows或其他非Unix体系的操作系统,最好将它们的工作方式暂时忘掉,因为这些记忆会干扰我们理解Linux底层的一些细节。
Linux驱动的工作和访问方式是Linux的亮点之一,同时受到了业界的广泛好评。Linux系统将每一个驱动都映射成一个文件。这些文件称为设备文件或驱动文件,都保存在/dev目录中。这种设计理念使得与Linux驱动进行交互就像与普通文件进行交互一样容易。当然,也比访问LinuxAPI更容易。由于大多数Linux驱动都有与其对应的设备文件,因此与Linux驱动交换数据就变成了与设备文件交换数据。例如,向Linux打印机驱动发送一个打印命令,可以直接使用C语言函数open打开设备文件,再使用C语言函数ioctl向该驱动的设备文件发送打印命令。
当然,要编写Linux驱动程序还需要更高级的功能。如向打印机驱动写入数据时,对于打印机驱动来说,需要接收这些被写入的数据,并将它们通过PC的并口、USB等端口发送给打印机。要实现这一过程就需要Linux驱动可以响应应用程序传递过来的数据。这就是Linux驱动的事件,虽然在C语言里没有事件的概念,但却有与事件类似的概念,这就是回调(callback)函数。因此,编写Linux驱动最重要的一步就是编写回调函数,否则与设备文件交互的数据将无法得到处理。图6-1是应用软件、设备文件、驱动程序、硬件之间的关系
二、编写Linux驱动程序的步骤
Linux驱动程序与其他类型的Linux程序一样,也有自己的规则。对于刚开始接触Linux驱动开发的读者可能对如何开发一个LInux驱动程序还不是太了解。为了解决这部分读者的困惑,本节给出了编写一个基本的Linux驱动的一般步骤。读者可以按着这些步骤循序渐进地学习Linux驱动开发。
第1步:建立Linux驱动骨架(装载和卸载Linux驱动)
任何类型的程序都有一个基本的结构,例如,C语言需要有一个入口函数main。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驱动模块。
上面8步中的前5步是关于如何编写Linux驱动程序的,通过后3步可以使Linux驱动正常工作。
三、编写Linux驱动程序前的准备工作
本例的Linux驱动源代码并未与linux内核源代码放在一起,而是单独放在一个目录。首先使用下面的命令建立存放Linux驱动程序的目录。
# mkdir –p /root/drivers/ch06/word_count
# cd /root/drivers/ch06/word_count
然后使用下面的命令建立驱动源代码文件(word_count.c)
# echo '' > word_count.c
最后编写一个Makefile文件,实际上这是6.2节介绍的编写Linux驱动程序的第6步。当熟悉编写Linux驱动程序的步骤后可以不按6.2节介绍的顺序来编写Linux驱动。
# 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内核源代码目录>/drivers/char目录找到一个built-in.o文件。该目标文件包含了所有可连接进Linux内核的字符驱动(通过make
menuconfig命令可以配置每一个驱动及其他内核程序是否允许编译进内核,关于配置Linux内核的技术详见4.2.4节介绍)。
如果Linux驱动依赖其他程序,如process.c、data.c。需要按如下方式编写Makefile文件。
obj-m := word_count.o
word_count-y := process.o data.o
其中依赖文件要使用module-y或module-objs指定。module表示模块名,如word_count。
四、编写Linux驱动程序的骨架
现在编写Linux驱动程序的骨架部分,也就是前面介绍的第1步。骨架部分主要是Linux驱动的初始化和退出函数,代码如下:
#include
#include
#include
#include
#include
#include
// 初始化Linux驱动
static int word_count_init(void)
{
// 输出日志信息
printk("word_count_init_success\n");
return 0;
}
// 退出Linux驱动
static void word_count_exit(void)
{
// 输出日志信息
printk("word_count_init_exit_success\n");
}
// 注册初始化Linux驱动的函数
module_init(word_count_init);
// 注册退出Linux驱动的函数
module_exit(word_count_exit);
|
在上面的代码中使用了printk函数。该函数用于输出日志信息(关于printk函数的详细用法将在10.1节详细介绍)。printk函数与printf函数的用法类似。有的读者可能会有疑问,为什么不用printf函数呢?这里就涉及到一个Linux内核程序可以调用什么,不可以调用什么的问题。Linux系统将内存分为了用户空间和内核空间,这两个空间的程序不能直接访问。printf函数运行在用户空间,printk函数运行在内核空间。因此,属于内核程序的Linux驱动是不能直接访问printf函数的。就算包含了stdio.h头文件,在编译Linux驱动时也会抛出stdio.h文件没找到的错误。当然,运行在用户空间的程序也不能直接调用printk函数。那么是不是用户空间和内核空间的程序就无法交互了呢?答案是否定的。否则这两块内存不就成了孤岛了吗。运行在这两块内存中的程序之间交互的方法很多。其中设备文件就是一种主要的交互方式(在后面的章节还会介绍/proc虚拟文件的交互方式)。如果用户空间的程序要访问内核空间,只要做一个可以访问内核空间的驱动程序,然后用户空间的程序通过设备文件与驱动程序进行交互即可。
看到这可能有的读者疑问更大了。Linux驱动程序无法直接访问运行在用户空间的程序,那么很多功能就都得自己实现了。例如,在C语言中会经常使用malloc函数动态分配内存空间,该函数在Linux驱动程序中是无法使用的。那么如何在Linux驱动程序中动态分配内存空间呢?解决类似的问题也很简单。既然Linux驱动无法直接调用运行在用户空间的函数,那么在Linux内核中就必须要提供替代品。读者可以进入<Linux内核源代码>/include目录,该目录的各个子目录中包含了大量的C语言头文件。这些头文件中定义的函数、宏等资源就是运行在用户空间的程序的替代品。运行在用户空间的函数库对应的头文件在/usr/include目录中。刚才提到的malloc函数在内核空间的替代品是kmalloc(需要包含slab.h头文件,#include
<linux/slab.h>)。
注意:用户空间与内核空间完成同样或类似功能的函数、宏等资源的名称并不一定相同,有的名称类似,如malloc和kmalloc,有的完全是两个不同的名字:如atoi(用户空间)和simple_strtol(内核空间)、itoa(用户空间)和snprintf(内核空间)。读者在使用内核相关资源时要注意在一点。
如果读者想看看前面编写的程序的效果,可以使用下面的命令编译Linux驱动源代码(X86架构)。
# make -C /usr/src/linux-headers-3.0.0-15-generic M=/root/drivers/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)。
注意:如果读者使用的Linux发行版采用了其他Linux内核,需要为-C命令行参数设置正确的路径。
执行上面的命令后,会输出如图6-2所示信息。从这些信息可以看出,已经将word_count.c文件编译成了Linux驱动模块文件word_count.ko。
使用ls命令列出/root/drivers/ch06/word_count目录中的文件后发现,除了多了几个.o和.ko文件,还多了一些其他的文件,如图6-3所示。这些文件是有编译器自动生成的,一般并不需要管这些文件的内容。
本文编写的Linux驱动程序虽然什么实际的功能都没有,但已经可以作为驱动程序安装在Linux内核空间了。读者可以使用下面的命令安装、查看、卸载Linux驱动,也可以查看由驱动程序输出的日志信息(执行下面命令时需要先进入word_count目录)。
安装Linux驱动
# insmod word_count.ko
查看word_count是否成功安装
# lsmod | grep word_count
卸载Linux驱动
# rmmod word_count
查看由Linux驱动输出的日志信息
# dmesg | grep word_count | tail –n 2
执行上面的命令后,如果输出如图6-4所示的信息说明读者已成功完成本节的学习,可以继续看下一节了。
dmesg命令实际上是从/var/log/messages(Ubuntu Linux 10.04)或/var/log/syslog(Ubuntu
Linux11.10)文件中读取的日志信息,因此也可以执行下面的命令获取由Linux驱动输出的日志信息。
# cat /var/log/syslog | grep word_count | tail –n 2
执行上面的命令后会输出更多的信息,如图6-5所示。
五、指定与驱动相关的信息
虽然指定这些信息不是必须的,但一个完整的Linux驱动程序都会指定这些与驱动相关的信息。一般需要为Linux驱动程序指定如下信息。
1. 模块作者:使用MODULE_AUTHOR宏指定。
2. 模块描述:使用MODULE_DESCRIPTION宏指定。
3. 模块别名:使用MODULE_ALIAS宏指定。
4. 开源协议:使用MODULE_LICENSE宏指定。
除了这些信息外,Linux驱动模块自己还会包含一些信息。读者可以执行下面的命令查看word_count.ko的信息。
# modinfo word_count.ko
执行上面的命令后,会输出如图6-6所示的信息。其中depends表示当前驱动模块的依赖,word_count并没有依赖什么,因此该项为空。vermagic表示当前Linux驱动模块在那个Linux内核版本下编译的。
现在使用下面的代码指定上述4种信息。一般会将这些代码放在word_count.c文件的最后。
MODULE_AUTHOR("lining");
MODULE_DESCRIPTION("statistics of wordcount.");
MODULE_ALIAS("word count module.");
MODULE_LICENSE("GPL");
现在使用上一节的方法重新编译word_count.c文件。然后再执行modinfo命令,就会显示如图6-7所示的信息。从图6-7可以看出,上面的代码设置的信息都包含在了word_count.ko文件中。
六、开源协议
虽然很多个人开发者或小公司并不太考虑开源协议的限制,但对于较大的公司如果违反开源协议,可能会有被起诉的风险。所以对有一定规模和影响力的公司使用开源软件时一定要注意这些软件使用的开源协议。
为了降低发布Linux驱动的难度和安装包尺寸,很多Linux驱动都是开放源代码的。在Linux驱动源代码中使用MODULE_LICENSE宏指定开源协议。例如,word_count驱动使用了GPL协议。那么我们要编写Linux'驱动程序应采取什么协议呢?目前开源协议比较多。读者可以到下面的页面查看所有的开源协议。
http://www.opensource.org/licenses/alphabetical
下面将介绍最常用的5种开源协议的基本情况。这5种开源协议以及其他的开源协议的详细情况请参阅Open
SourceInitiative组织的相关页面。
GPL协议
对于喜欢钻研技术的程序员来说应该很喜欢GPL协议。因为GPL协议强迫使用该开源协议的软件开源。例如,Linux内核就采用了GPL协议。GPL的出发点就是免费/开源。但与其他开源协议(如BSD、Apache
Licence)不同的是GPL协议开源的更彻底。不仅要求采用GPL协议的软件开源/免费,还要求其衍生代码开源/免费。例如,A软件采用了GPL协议,B软件使用了A软件,那么B软件也必须免费/开源。而其B软件必须也采用GPL协议。C软件又使用了B软件,C软件也必须开源/免费,当然,C软件也必须采用GPL协议。这就是所谓的“传染性”。这也是目前有很多Linux发行版及其他使用GPL协议的软件开源的原因,
由于GPL协议严格要求使用了GPL协议的软件产品必须使用GPL协议,而且必须开源/免费。对于商业软件或者对代码有保密要求的部门就非常不适合使用GPL协议发布软件,或引用基于GPL协议的类库。为了满足商业公司及保密的需要,在GPL协议的基础上又出现了LGPL协议。
LGPL协议
LGPL主要是为类库使用设计的开源协议。与GPL要求任何使用/修改/衍生的GPL类库的的软件必须采用GPL协议不同。LGPL
允许商业软件通过类库引用(link)方式使用LGPL类库而不需要开源商业软件的代码。这使得采用LGPL协议的开源代码可以被商业软件作为类库引用并发布和销售。
但是如果修改LGPL协议的代码或者衍生,则所有修改的代码,涉及修改部分的额外代码和衍生的代码都必须采用LGPL协议。因此LGPL协议的开源代码很适合作为第三方类库被商业软件引用,但不适合希望以LGPL协议代码为基础,通过修改和衍生的方式做二次开发的商业软件采用。
BSD协议
BSD开源协议是一个给于使用者很大自由的协议。基本上使用者可以“为所欲为”,可以自由的使用,修改源代码,也可以将修改后的代码作为开源或者专有软件再发布。但“为所欲为”的前提是当你发布使用了BSD协议的代码,或则以BSD协议代码为基础做二次开发自己的产品时,需要满足如下3个条件。
1. 如果再发布的产品中包含源代码,则在源代码中必须带有原来代码中的BSD协议。
2. 如果再发布的只是二进制类库/软件,则需要在类库/软件的文档和版权声明中包含原来代码中的BSD协议。
3. 不可以用开源代码的作者/机构名字和原来产品的名字做市场推广。
BSD 协议鼓励代码共享,但需要尊重源代码作者的著作权。BSD由于允许使用者修改和重新发布代码,也允许使用或在BSD代码上开发商业软件发布和销售,因此是对商业集成很友好的协议。而很多的公司企业在选用开源产品的时候都首选BSD协议,因为可以完全控制这些第三方的代码,在必要的时候可以修改或者二次开发。
Apache Licence 2.0协议
Apache Licence是著名的非盈利开源组织Apache采用的协议。该协议和BSD类似,同样鼓励代码共享和尊重原作者的著作权,同样允许代码修改,再发布(作为开源或商业软件)。需要满足的条件也和BSD类似。
1. 需要给代码的用户一份Apache Licence
2. 如果你修改了代码,需要在被修改的文件中说明。
3. 在延伸的代码中(修改和由源代码衍生的代码中)需要带有原来代码中的协议,商标,专利声明和其他原来作者规定需要包含的说明。
4. 如果再次发布的产品中包含一个Notice文件,则在Notice文件中需要带有Apache Licence。你可以在Notice中增加自己的许可,但不可以表现为Apache
Licence。
Apache Licence也是对商业应用友好的许可。使用者也可以在需要的时候修改代码来满足需要并作为开源或商业产品发布/销售。
MIT协议
MIT是和BSD一样限制宽松的许可协议,作者只想保留版权,而无任何其他了限制.也就是说,你必须在你的发行版里包含原许可协议的声明,无论你是以二进制发布的还是以源代码发布的。
七、注册和注销设备文件
本节将为word_count驱动建立一个设备文件,该设备文件的名称是wordcount,位于/dev目录中。设备文件与普通文件不同,不能使用IO函数建立,需要使用misc_register函数建立设备文件,使用misc_deregister函数注销(移除)设备文件。这两个函数的定义如下:
extern int misc_register(struct miscdevice * misc);
extern int misc_deregister(struct miscdevice*misc);
|
一般需要在初始化Linux驱动时建立设备文件,在卸载Linux驱动时删除设备文件。而且设备文件还需要一个结构体(miscdevice)来描述与其相关的信息。miscdevice结构体中有一个重要的成员变量fops,用于描述设备文件在各种可触发事件的函数指针。该成员变量的数据类型也是一个结构体file_operations。
本节需要修改word_count.c文件的word_count_init和word_count_exit函数,并定义一些宏和变量。修改部分的代码如下:
// 定义设备文件名
#define DEVICE_NAME "wordcount"
// 描述与设备文件触发的事件对应的回调函数指针
// owner:设备事件回调函数应用于哪些驱动模块,THIS_MODULE表示应用于当前驱动模块
static struct file_operations dev_fops =
{ .owner = THIS_MODULE};
// 描述设备文件的信息
// minor:次设备号 MISC_DYNAMIC_MINOR,:动态生成次设备号 name:设备文件名称
// fops:file_operations结构体变量指针
static struct miscdevice misc =
{ .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME,.fops = &dev_fops };
// 初始化Linux驱动
static int word_count_init(void)
{
int ret;
// 建立设备文件
ret = misc_register(&misc);
// 输出日志信息
printk("word_count_init_success\n");
return ret;
}
// 卸载Linux驱动
static void word_count_exit(void)
{
// 注销(移除)设备文件
misc_deregister(&misc);
// 输出日志信息
printk("word_init_exit_success\n");
}
|
编写上面代码需要注意如下几点:
1. 设备文件由主设备号和次设备号描述。而使用misc_register函数只能设置次设备号。主设备号统一设为10。主设备号为10的设备是Linux系统中拥有共同特性的简单字符设备。这类设备称为misc设备。如果读者实现的驱动的功能并不复杂,可以考虑使用10作为其主设备号,而次设备号可以自己指定,也可以动态生成(需要指定MISC_DYNAMIC_MINOR常量)。因为采用这样的方式可以使用misc_register和misc_deregister函数简化注册和注销设备文件的步骤。在后面的章节会详细介绍如何使用register_chrdev_region和alloc_chrdev_region函数同时指定主设备号和次设备号的方式注册和注销设备文件。
2. miscdevice.name变量的值就是设备文件的名称。在本例中设备文件名称为wordcount。
3. 虽然file_operations结构体中定义了多个回调函数指针变量,但本节并未初始化任何一个回调函数指针变量。只初始化了file_operations.owner变量。如果该变量的值为module结构体,表示file_operations可被应用在这些由module指定的驱动模块中。如果owner变量的值为THIS_MODULE,表示file_operations只应用于当前驱动模块。
4. 如果成功注册了设备文件,misc_register函数返回非0的整数,如果注册设备文件失败,返回0。
5. 可能有的读者注意到了。word_count.c中的所有函数、变量都声明成了static。这是因为在C语言中用static声明函数、变量等资源,系统会将这些函数和变量单独放在内存的某一个区域,直到程序完全退出,否则这些资源不会被释放。Linux驱动一旦装载,除非手动卸载或关机,驱动会一直驻留内存,因此这些函数和变量资源会一直在内存中。也就是说多次调用这些资源不用再进行压栈、出栈操作了。有利于提高驱动的运行效率。
现在重新编译word_count.c文件并使用如下的命令安装word_count驱动。
# insmod word_count.ko
如果word_count驱动已经被安装,应先使用下面的命令下载word_count驱动,然后再使用上面的命令安装word_count驱动。
# rmmod word_count
安装完word_count驱动后,使用下面的命令查看/dev目录中的设备。
# ls –a /dev
执行上面的命令后,会输出如图6-8所示的信息,其中多了一个wordcount文件(在白框中)。
如果想查看wordcount设备文件的主设备号和次设备号,可以使用如下的命令。
# ls –l /dev
执行上面的命令会输出如图6-9所示的信息,白框中的第一个数字是主设备号,第二个数字是从设备号。
使用下面的命令可获显示当期系统中有哪些主设备以及主设备号。
# cat /proc/devices
执行上面的命令后会输出如图6-10所示的信息,从中可以找到misc设备以及主设备编号10。
八、 指定回调函数
本节讲的内容十分关键。不管Linux驱动程序的功能多么复杂还是多么“酷”,都必须允许用户空间的应用程序与内核空间的驱动程序进行交互才有意义。而最常用的交互方式就是读写设备文件。通过file_operations.read和file_operations.write成员变量可以分别指定读写设备文件要调用的回调函数指针。
在本节将为word_count.c添加两个函数:word_count_read和word_count_write。这两个函数分别处理从设备文件读数据和向设备文件写数据的动作。本节的例子先不考虑word_count要实现的统计单词数的功能,先用word_count_read和word_count_write函数做一个读写设备文件数据的实验,以便让读者了解如何与设备文件交互数据。本节编写的word_count.c文件是一个分支,读者可在word_count/read_write目录找到word_count.c文件。可以用该文件覆盖word_count目录下的同名文件测试本节的例子。
本例的功能是向设备文件/dev/wordcount写入数据后,都可以从/dev/wordcount设备文件中读出这些数据(只能读取一次)。下面先看看本例的完整的代码。
#include
#include
#include
#include
#include
#include
#define DEVICE_NAME "wordcount" // 定义设备文件名
static unsigned char mem[10000]; // 保存向设备文件写入的数据
static char read_flag = 'y'; // y:已从设备文件读取数据 n:未从设备文件读取数据
static int written_count = 0; // 向设备文件写入数据的字节数
// 从设备文件读取数据时调用该函数
// file:指向设备文件、buf:保存可读取的数据 count:可读取的字节数 ppos:读取数据的偏移量
static ssize_t word_count_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
// 如果还没有读取设备文件中的数据,可以进行读取
if(read_flag == 'n')
{
// 将内核空间的数据复制到用户空间,buf中的数据就是从设备文件中读出的数据
copy_to_user(buf, (void*) mem, written_count);
// 向日志输出已读取的字节数
printk("read count:%d", (int) written_count);
// 设置数据已读状态
read_flag = 'y';
return written_count;
}
// 已经从设备文件读取数据,不能再次读取数据
else
{
return 0;
}
}
// 向设备文件写入数据时调用该函数
// file:指向设备文件、buf:保存写入的数据 count:写入数据的字节数 ppos:写入数据的偏移量
static ssize_t word_count_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
// 将用户空间的数据复制到内核空间,mem中的数据就是向设备文件写入的数据
copy_from_user(mem, buf, count);
// 设置数据的未读状态
read_flag = 'n';
// 保存写入数据的字节数
written_count = count;
// 向日志输出已写入的字节数
printk("written count:%d", (int)count);
return count;
}
// 描述与设备文件触发的事件对应的回调函数指针
// 需要设置read和write成员变量,系统才能调用处理读写设备文件动作的函数
static struct file_operations dev_fops =
{ .owner = THIS_MODULE, .read = word_count_read, .write = word_count_write };
// 描述设备文件的信息
static struct miscdevice misc =
{ .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops };
// 初始化Linux驱动
static int word_count_init(void)
{
int ret;
// 建立设备文件
ret = misc_register(&misc);
// 输出日志信息
printk("word_count_init_success\n");
return ret;
}
// 卸载Linux驱动
static void word_count_exit(void)
{
// 删除设备文件
misc_deregister(&misc);
// 输出日志信息
printk("word_init_exit_success\n");
}
// 注册初始化Linux驱动的函数
module_init( word_count_init);
// 注册卸载Linux驱动的函数
module_exit( word_count_exit);
MODULE_AUTHOR("lining");
MODULE_DESCRIPTION("statistics of word count.");
MODULE_ALIAS("word count module.");
MODULE_LICENSE("GPL");
|
编写上面代码需要了解如下几点。
1. word_count_read和word_count_write函数的参数基本相同,只有第2个参数buf稍微一点差异。word_count_read函数的buf参数类型是char*,而word_count_write函数的buf参数类型是const
char*,这就意味着word_count_write函数中的buf参数值无法修改。word_count_read函数中的buf参数表示从设备文件读出的数据,也就是说,buf中的数据都可能由设备文件读出,至于可以读出多少数据,取决于word_count_read函数的返回值。如果word_count_read函数返回n,则可以从buf读出n个字符。当然,如果n为0,表示无法读出任何的字符。如果n小于0,表示发生了某种错误(n为错误代码)。word_count_write函数中的buf表示由用户空间的应用程序写入的数据。buf参数前有一个“__user”宏,表示buf的内存区域位于用户空间。
2. 由于内核空间的程序不能直接访问用户空间中的数据,因此,需要在word_count_read和word_count_write函数中分别使用copy_to_user和copy_from_user函数将数据从内核空间复制到用户空间或从用户空间复制到内核空间。
3. 本例只能从设备文件读一次数据。也就是说,写一次数据,读一次数据后,第二次无法再从设备文件读出任何数据。除非再次写入数据。这个功能是通过read_flag变量控制的。当read_flag变量值为n,表示还没有读过设备文件,在word_count_read函数中会正常读取数据。如果read_flag变量值为y,表示已经读过设备文件中的数据,word_count_read函数会直接返回0。应用程序将无法读取任何数据。
4. 实际上word_count_read函数的count参数表示的就是从设备文件读取的字节数。但因为使用cat命令测试word_count驱动时。直接读取了32768个字节。因此count参数就没什么用了(值总是32768)。所以要在word_count_write函数中将写入的字节数保存,在word_count_read函数中直接使用写入的字节数。也就是说,写入多少个字节,就读出多少个字节。
5. 所有写入的数据都保存在mem数组中。该数组定义为10000个字符,因此写入的数据字节数不能超过10000,否则将会溢出。
为了方便读者测试本节的例子,笔者编写了几个Shell脚本文件,允许在UbuntuLinux、S3C6410开发板和Android模拟器上测试word_count驱动。其中有一个负责调度的脚本文件build.sh。本书所有的例子都会有一个build.sh脚本文件,执行这个脚本文件就会要求用户选择将源代码编译到那个平台,选择菜单如图6-11所示。用户可以输入1、2或3选择编译平台。如果直接按回车键,默认值会选择第1个编译平台(UbuntuLinux)。
build.sh脚本文件的代码如下:
source /root/drivers/common.sh
# select_target是一个函数,用语显示图6-11所示的选择菜单,并接收用户的输入
# 改函数在common.sh文件中定义
select_target
if [ $selected_target == 1 ]; then
source ./build_ubuntu.sh # 执行编译成Ubuntu Linux平台驱动的脚本文件
elif [ $selected_target == 2 ]; then
source ./build_s3c6410.sh # 执行编译成s3c6410平台驱动的脚本文件
elif [ $selected_target == 3 ]; then
source ./build_emulator.sh # 执行编译成Android模拟器平台驱动的脚本文件
fi
|
在build.sh脚本文件中涉及到了3个脚本文件(build_ubuntu.sh、build_s3c6410.sh和build_emulator.sh),这3个脚本文件的代码类似,只是选择的Linux内核版本不同。对于S3C6410和Android模拟器平台,编译完后Linux驱动,会自动将编译好的Linux驱动文件(*.so文件)上传到相应平台的/data/local目录,并安装Linux驱动。例如,build_s3c6410.sh脚本文件的代码如下:
source /root/drivers/common.sh
# S3C6410_KERNEL_PATH变量是适用S3C6410平台的Linux内核源代码的路径,
# 该变量以及其它类似变量都在common.sh脚本文件中定义
make -C $S3C6410_KERNEL_PATH M=${PWD}
find_devices
# 如果什么都选择,直接退出
if [ "$selected_device" == "" ]; then
exit
else
# 上传驱动程序(word_count.ko)
adb -s $selected_device push ${PWD}/word_count.ko /data/local
# 判断word_count驱动是否存在
testing=$(adb -s $selected_device shell lsmod | grep "word_count")
if [ "$testing" != "" ]; then
# 删除已经存在的word_count驱动
adb -s $selected_device shell rmmod word_count
fi
# 在S3C6410开发板中安装word_count驱动
adb -s $selected_device shell "insmod /data/local/word_count.ko"
fi
|
使用上面的脚本文件,需要在read_write目录建立一个Makefile文件,内容如下:
obj-m := word_count.o
现在执行build.sh脚本文件,选择要编译的平台,并执行下面的命令向/dev/word_count设备文件写入数据。
# echo ‘hello lining’ > /dev/wordcount
然后执行如下的命令从/dev/word_count设备文件读取数据。
# cat /dev/wordcount
如果输出“hello lining”,说明测试成功。
注意:如果在S3C6410开发板和Android模拟器上测试word_count驱动,需要执行shell.sh脚本文件或adb
shell命令进入相应平台的终端。其中shell.sh脚本在/root/drivers目录中。这两种方式的区别是如果有多个Android设备和PC相连时,shell.sh脚本会出现一个类似图6-11所示的选择菜单,用户可以选择进入哪个Android设备的终端,而adb
shell命令必须要加-s命令行参数指定Android设备的ID才可以进入相应Android设备的终端。
九、实现统计单词数的算法
本节开始编写word_count驱动的业务逻辑:统计单词数。本节实现的算法将由空格、制表符(ASCII:9)、回车符(ASCII:13)和换行符(ASCII:10)分隔的字符串算做一个单词,该算法同时考虑了有多个分隔符(空格符、制表符、回车符和换行符)的情况。下面是word_count驱动完整的代码。在代码中包含了统计单词数的函数get_word_count。
#include
#include
#include
#include
#include
#include
#define DEVICE_NAME "wordcount" // 定义设备文件名
static unsigned char mem[10000]; // 保存向设备文件写入的数据
static int word_count = 0; // 单词数
#define TRUE -1
#define FALSE 0
// 判断指定字符是否为空格(包括空格符、制表符、回车符和换行符)
static char is_spacewhite(char c)
{
if(c == ' ' || c == 9 || c == 13 || c == 10)
return TRUE;
else
return FALSE;
}
// 统计单词数
static int get_word_count(const char *buf)
{
int n = 1;
int i = 0;
char c = ' ';
char flag = 0; // 处理多个空格分隔的情况,0:正常情况,1:已遇到一个空格
if(*buf == '\0')
return 0;
// 第1个字符是空格,从0开始计数
if(is_spacewhite(*buf) == TRUE)
n--;
// 扫描字符串中的每一个字符
for (; (c = *(buf + i)) != '\0'; i++)
{
// 只由一个空格分隔单词的情况
if(flag == 1 && is_spacewhite(c) == FALSE)
{
flag = 0;
}
// 由多个空格分隔单词的情况,忽略多余的空格
else if(flag == 1 && is_spacewhite(c) == TRUE)
{
continue;
}
// 当前字符为空格时单词数加1
if(is_spacewhite(c) == TRUE)
{
n++;
flag = 1;
}
}
// 如果字符串以一个或多个空格结尾,不计数(单词数减1)
if(is_spacewhite(*(buf + i - 1)) == TRUE)
n--;
return n;
}
// 从设备文件读取数据时调用的函数
static ssize_t word_count_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
unsigned char temp[4];
// 将单词数(int类型)分解成4个字节存储在buf中
temp[0] = word_count >> 24;
temp[1] = word_count >> 16;
temp[2] = word_count >> 8;
temp[3] = word_count;
copy_to_user(buf, (void*) temp, 4);
printk("read:word count:%d", (int) count);
return count;
}
// 向设备文件写入数据时调用的函数
static ssize_t word_count_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
ssize_t written = count;
copy_from_user(mem, buf, count);
mem[count] = '\0';
// 统计单词数
word_count = get_word_count(mem);
printk("write:word count:%d", (int)word_count);
return written;
}
// 描述与设备文件触发的事件对应的回调函数指针
static struct file_operations dev_fops =
{ .owner = THIS_MODULE, .read = word_count_read, .write = word_count_write };
// 描述设备文件的信息
static struct miscdevice misc =
{ .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops };
// 初始化Linux驱动
static int word_count_init(void)
{
int ret;
// 建立设备文件
ret = misc_register(&misc);
// 输出日志信息
printk("word_count_init_success\n");
return ret;
}
// 卸载Linux驱动
static void word_count_exit(void)
{
// 删除设备文件
misc_deregister(&misc);
// 输出日志信息
printk("word_init_exit_success\n");
}
// 注册初始化Linux驱动的函数
module_init( word_count_init);
// 注册卸载Linux驱动的函数
module_exit( word_count_exit);
MODULE_AUTHOR("lining");
MODULE_DESCRIPTION("statistics of word count.");
MODULE_ALIAS("word count module.");
MODULE_LICENSE("GPL");
|
编写word_count驱动程序需要了解如下几点。
1. get_word_count函数将mem数组中第1个为“\0”的字符作为字符串的结尾符,因此在word_count_write函数中将mem[count]的值设为“\0”,否则get_word_count函数无法知道要统计单词数的字符串到哪里结束。由于mem数组的长度为10000,而字符串最后一个字符为“\0”,因此待统计的字符串最大长度为9999。
2. 单词数使用int类型变量存储。在word_count_write函数中统计出了单词数(word_count变量的值),在word_count_read函数中将word_count整型变量值分解成4个字节存储在buf中。因此,在应用程序中需要再将这4个字节组合成int类型的值。
十、编译、安装、卸载Linux驱动程序
在上一节word_count驱动程序已经全部编写完成了,而且多次编译测试该驱动程序。安装和卸载word_count驱动也做过多次。word_count驱动与read_write目录中的驱动一样,也有一个build.sh和3个与平台相关的脚本文件。这些脚本文件与6.3.5节的实现类似,这里不再详细介绍。现在执行build.sh脚本文件,并选择要编译的平台。然后执行下面两行命令查看日志输出信息和word_count驱动模块(word_count.ko)的信息。
# dmesg |tail -n 1
# modinfo word_count.ko
如果显示如图6-12所示的信息,表明word_count驱动工作完全正常。
本书的脚本文件都是使用insmod命令安装Linux驱动的,除了该命令外,使用modprobe命令也可以安装Linux驱动。insmod和modprobe的区别是modprobe命令可以检查驱动模块的依赖性。如A模块依赖于B模块(装载A之前必须先装载B)。如果使用insmod命令装载A模块,会出现错误。而使用modprobe命令装载A模块,B模块会现在装载。在使用modprobe命令装载驱动模块之前,需要先使用depmod命令检测Linux驱动模块的依赖关系。
# depmod /root/drivers/ch06/word_count/word_count.ko
depmod命令实际上将Linux驱动模块文件(包括其路径)添加到如下的文件中。
/lib/modules/3.0.0-16-generic/modules.dep
使用depmod命令检测完依赖关系后,就可以调用modprobe命令装载Linux驱动。
# modprobe word_count
使用depmod和modprobe命令需要注意如下几点:
1. depmod命令必须使用Linux驱动模块(.ko文件)的绝对路径。
2. depmod命令会将内核模块的依赖信息写入当前正在使用的内核的modules.dep文件。例如,笔者的Ubuntu
Linux使用的是Linux3.0.0.16,所以应到3.0.0-16-generic目录去寻找modules.dep文件。如果读者使用了其他Linux内核,需要到相应的目录去寻找modules.dep文件。
3. modprobe命令只需使用驱动名称即可,不需要跟.ko。
|