第六章 第一个Linux驱动程序:统计单词个数

时间:2021-04-06 00:05:48

现在进入了实战阶段,使用统计单词个数的实例让我们了解开发和测试Linux驱动程序的完整过程。第一个Linux驱动程序是统计单词个数。

这个Linux驱动程序没有访问硬件,而是利用设备文件作为介质与应用程序交互,应用程序通过向设备文件传递一个由空格分隔的字符串,将每一个被空格隔开的子字符串看作一个单词,然后从设备文件读出来的是该字符串包含的单词个数。在编写此Linux驱动程序前需要做一些准备工作,先使用命令

“# mkdir -p /root/drivers/ch06/word_count

 # cd /root/drivers/ch06/word_count”建立存放Linux驱动程序的目录。再执行命令“# echo ''>word_count.c”建立驱动源代码文件。最后编写Makefile文件,可执行命令“# echo 'obj-m := word_count.o'> Makefile”,obj-m表示将Linux驱动作为后缀为.ko的模块进行编译。obj-y则表示将Linux驱动编译进Linux内核。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内核的字符驱动,可通过make menuconfig命令配置每一个驱动及其他内核程序是否允许编译进内核。如果Linux驱动依赖其他程序,如process.c、data.c,则需使用如下命令

“obj-m :=word_count.o

word_count-y :=process.o data.o”编写Makefile文件,其中依赖文件要使用module-y或module-objs指定,module表示模块名。

编写Linux驱动程序的骨架,即初始化和退出驱动函数。其中使用了printk(),该函数用于输出日志信息,与printf()用法类似。Linux系统将内存分为用户空间和内核空间,这两个空间的程序不能直接访问。printf运行在用户空间,printk运行在内核空间,因此,属于内核程序的Linux驱动是不能直接访问printf函数的,即使包含了头文件stdio.h,在编译Linux驱动时也会抛出找不到头文件stdio.h的错误。运行在用户空间的程序也不能直接调用printk。但运行在用户空间和内核空间的程序之间还是可以进行交互的。设备文件就是一种主要的交互方式。若用户空间的程序要访问内核空间,只要做一个可以访问内核空间的驱动程序,然后用户空间的程序通过设备文件与驱动程序进行交互即可。Linux驱动程序无法直接访问运行在用户空间的程序,很多功能就需要自己实现。在C语言中使用malloc来动态分配内存空间,但该函数在Linux驱动程序中是无法使用的。这就必须要在Linux内核中提供替代品。在<Linux内核源代码>/include目录中的各个子目录包含了大量的C语言头文件,其中定义的函数、宏等资源就是运行在用户空间的程序的替代品。运行在用户空间的函数库对应的头文件在/usr/include目录中,如malloc函数在内核空间的替代品是kmalloc,使用它需要包含头文件slab.h。用户空间与内核空间完成同样或类似功能的函数、宏等资源的名称并不一定相同,但可能类似,也有完全是两个不同名字的,如用户空间的atoi和内核空间的simple_strtol,以及用户空间的itoa和内核空间的snprintf等。

在测试Linux驱动时未必一定要在Android设备上完成,因为Android系统和Ubuntu以及Linux发行版本都是基于Linux内核的,大多数Linux驱动程序可以在Ubuntu或其他Linux发行版上测试完再重新用交叉编译器生成基于ARM架构的目标文件,然后安装到Android上即可正常运行。编译Linux内核源代码需使用Linux内核的头文件。为了在Ubuntu上测试驱动程序,需要使用-C命令行参数指定Linux内核头文件的目录,/usr/src/linux-headers-3.0.0-15-generic,其中linux-headers-3.0.0-15-generic目录是Linux内核源代码目录,为了开发当前Linux内核版本的驱动及其他内核程序而提供的,该目录中只有include子目录有实际的头文件,其他目录只有Makefile和其他一些配置文件,并不包含Linux内核源代码,因为在编译Linux驱动时生成的目标文件只需要头文件,在进行目标文件链接时只要相关的目标文件即可,并不需要源代码文件。如果以模块方式编译Linux驱动程序,需要使用M指定驱动程序所在的目录,M=root/drivers/ch06/word_count。使用ls命令列出word_count目录中的文件后,会发现多了几个.o和.ko文件以及一些其他由编译器自动生成的文件。将驱动程序安装在内核空间,需先进入word_count目录下,使用命令“# insmod word_count.ko”安装Linux驱动,“# lsmod | grep word_count”查看word_count是否安装成功,“# rmmod word_count”用来卸载Linux驱动,还可使用命令“# dmesg | grep word_count | tail -n 2”或“# cat /var/log/syslog | grep word_count | tail -n 2”查看由Linux驱动输出的日志信息。

指定与驱动相关的信息,这些信息虽不是必须的,但一个完整的Linux驱动程序都会指定这些与驱动相关的信息,分别有:模块作者-使用MODULE_AUTHOR宏指定,模块描述-使用MODULE_DESCRIPTION宏指定,模块别名-使用MODULE_ALIS宏指定,开源协议-使用MODULE_LICENSE宏指定。除了这些信息,还可使用命令“# modinfo word_count.ko”查看word_count.ko的信息,这些信息也可通过代码的形式放在word_count.c文件的最后。之后重新编译,再执行modinfo命令,上面的代码设置的信息都包含在了word_count.ko中。

为了降低发布Linux驱动的难度和安装包装尺寸,很多Linux驱动都是开放源代码的。在Linux驱动源代码中使用MODULE_LICENSE宏指定开源协议。最常用的开源协议有5种:①GPL协议:Linux内核采用了GPL协议,GPL的出发点是免费开源,但与其他开源协议不同的是GPL协议开源的更彻底。不仅要求采用GPL协议的软件开源,还要求其衍生的代码开源免费,具有“传染性”。由于GPL协议严格要求使用了GPL协议的软件产品必须使用GPL协议,而且必须开源免费,所以对于商业软件或对代码有保密要求的部门就不适合使用GPL协议或引用基于GPL协议的类库发布软件。为了满足商业公司及保密的需要,产生了之后的协议②LGPL协议是为类库使用设计的开源协议,其衍生的协议不是必须采用该协议,LGPL允许商业软件通过类库引用方式使用LGPL类库而不需要开源商业软件的代码,这使得采用LGPL协议的开源代码可被商业软件作为类库引用并发布和销售。但若要修改LGPL协议的代码或衍生,则需修改所有的代码,涉及修改部分的额外代码和衍生代码都必须采用LGPL协议。LGPL协议的开源代码适合作为第三方类库被商业软件引用,但不适合以LGPL协议代码为基础,通过修改和衍生的方式做二次开发的商业软件采用③BSD协议给予使用者很大的*协议,可以*地使用、修改源代码,也可将修改后的代码作为开源或专有软件再发布。然而当发布使用BSD协议的代码,或以BSD协议代码为基础做二次开发自己的产品时,需满足3个条件:(1)再发布的产品中包含源代码,则在源代码中必须带有原来代码中的BSD协议(2)再发布的只是二进制类库/软件,需要在类库/软件的文档和版权声明中包含原来代码中的BSD协议(3)不可用开源代码的作者/机构名字和原来产品的名字做市场推广。BSD协议鼓励代码共享,但须尊重源代码作者的著作权。④Apache Licence2.0协议和BSD类似,允许代码修改再发布,需满足的条件也类似,(1)需要给代码的用户一份Apache Licence(2)若修改了代码,需要在被修改的文件中说明(3)在衍生的代码中需带有原来代码中的协议、商标、专利声明和其他原来作者规定需要包含的说明(4)再次发布的产品中包含一个notice文件,文件中需要带有Apache Licence,也可增加自己的许可⑤MIT协议限制宽松的许可协议,必须在发行版里包含原许可协议的声明,无论是二进制还是源代码发布。

注册和注销设备文件:以驱动word_count建立一个设备文件-wordcount。设备文件与普通文件不同,不能使用IO函数建立,需使用misc_register()建立设备文件,使用misc_deregister()注销设备文件。一般需在初始化Linux驱动时建立设备文件,在卸载Linux驱动时删除设备文件。且设备文件还需要一个结构体来描述与其相关的信息。miscdevice结构体中有一个成员变量fops,用于描述设备文件在各种可触发事件的函数指针,该成员变量的数据类型也是一个结构体file_operations。

需要修改word_count.c,编写代码需注意的有:设备文件由主设备号和次设备号描述。而使用misc_register()只能设置次设备号。主设备号统一设为10,它是Linux系统中拥有共同特性的简单字符设备。这类设备称为misc设备。次设备号可以自己指定也可动态生成,需指定MISC_DYNAMIC_MINOR常量,采用这样的方式可以使用misc_register和misc_deregister简化注册和注销设备文件的步骤。可使用register_chrdev_region和alloc_chrdev_region同时指定主设备号和次设备号的方式注册和注销设备文件;miscdevice.name变量的值就是设备文件的名称;file_operations结构体中定义了多个回调函数指针变量,只初始化了file_operations.owner变量,若该变量的值为module结构体,表示file_operations可被应用在这些由module指定的驱动模块中。若owner变量的值为THIS_MODULE,表示file_operations只应用于当前驱动模块;若成功注册了设备文件,misc_register返回非0的整数,若注册失败,返回0;word_count.c中的所有函数、变量都声明成了static,系统会将这些函数和变量单独放在内存的某一区域,直到程序完全退出,否则这些资源不会被释放。Linux驱动一旦装载,除非手动卸载或关机,驱动会一直驻留在内存中,这些函数和变量资源也会在内存中。,即多次调用这些资源不用再进行压栈、出栈操作了,有利于提高驱动的运行效率。编写后需重新编译word_count.c并使用如下命令“# insmod word_count.ko”安装word_count驱动。若驱动已被安装,应先使用命令“# rmmod word_count”下载驱动,然后再使用insmod命令安装驱动。安装完驱动后,执行命令“# ls -a /dev”查看/dev目录中的设备。执行完命令后,会多一个名为wordcount文件。可使用命令“# ls -l /dev”查看wordcount设备文件的主设备号和次设备号。执行上面命令后,白框中第一个数字是主设备号,第二个数字是从设备号。使用命令“# cat /proc/devices”可查看当前系统中有哪些主设备及主设备号。

指定回调函数:不管Linux驱动程序的功能多么复杂,都必须允许用户空间的应用程序与内核空间的驱动程序进行交互才有意义。最常用的交互方式就是读写设备文件。通过file_operations.read和file_operations.write成员变量可分别指定读写设备文件要调用的回调函数指针。要为word_count.c添加两个函数:word_count_read和word_count_write,分别处理从设备文件读数据和向设备文件写数据的动作。实例的功能是向设备文件/dev/wordcount写入数据,可从/dev/wordcount设备文件中读出这些数据,但只能读取一次。编写此实例需注意的有:word_count_read和word_count_write的参数基本相同,只有第2个参数buf稍有些差异。word_count_read的buf参数类型是char*,word_count_write的buf参数类型是const char*,意味着word_count_write中的参数值无法修改。word_count_read其参数表示从设备文件读出的数据,读出数据的多少,取决于word_count_read的返回值。word_count_write中的buf表示由用户空间的应用程序写入的数据。buf参数前有一个_user宏,表示buf的内存区域位于用户空间;由于内核空间的程序不能直接访问用户空间中的数据,需要在word_count_read和word_count_write中分别使用copy_to_user和copy_from_user函数将数据从内核空间复制到用户空间或反之;写一次数据,读一次数据后,第二次就无法再从设备文件读出任何数据,除非再写入数据。这是通过read_flag变量控制的,变量值为n,表示还没有读过设备文件,在word_count_read中会正常读取数据。若变量值为y,表示已经读过设备文件中的数据,word_count_read返回值为0,应用程序无法读取任何数据;word_count_read中的count参数表示的是从设备文件读取的字节数。使用cat命令测试驱动总是读取32768字节。在word_count_write中写入的字节数需保存,在word_count_read中直接使用写入的字节数,即写入多少字节,读出多少字节;所有写入的数据都保存在mem数组中,该数组定义10000个字符,因此写入的数据字节数不能超过10000,否则将溢出。如果在S3C6410开发板和Android模拟器上测试word_count驱动,需执行shell.sh脚本文件或adb shell命令进入相应平台的终端。shell.sh脚本在/root/drivers目录中。这两种方式的区别是如果有多个Android设备和PC相连时,脚本会出现一个选择菜单,用户可以选择进入哪个Android设备终端,而adb shell命令必须要加-s命令行参数指定Android设备的ID才可进入相应Android设备的终端。

实现统计单词数的算法:开始编写word_count驱动的业务逻辑-统计单词数。算法由空格、制表符、回车符和换行符分隔的字符串算作一个单词。代码中统计单词数的函数是get_word_count。需了解的有:get_word_count将mem数组中第一个为“\0”的字符作为字符串的结尾符,在word_count_write中将men[count]的值为“\0”,否则get_word_count将无法知道要统计单词数的字符串到哪里结束。mem数组长度为10000,而字符串最后一个字符为“\0”,所以待统计的字符串最大长度为9999;单词数使用int类型变量存储。在word_count_write中统计出单词数,在word_count_read中将word_count整型变量分解为4个字节存储在buf中,在应用程序中需将这4个字节组合成int类型的值。驱动程序已全部编写完成。

编译、安装、卸载Linux驱动程序:word_count驱动与read_write目录中的驱动一样,也有一个build.sh脚本文件和3个与平台相关的脚本文件。执行build.sh脚本文件,并选择编译的平台。执行命令

“# dmesg | tail -n 1

# modinfo word_count.ko”查看日志输出信息和word_count驱动模块信息。安装Linux驱动可使用insmod命令,也可使用modprobe命令。insmod和modprobe的区别是modprobe可以检查驱动模块的依赖性。如A模块依赖于B模块,则装载A之前必须先装载B。使用insmod装载A模块,会出现错误。在使用modprobe装载驱动模块之前,需先使用depmod命令“# depmod /root/drivers/ch06/word_count/word_count.ko”检测Linux驱动模块的依赖关系。depmod实际上将Linux驱动模块文件添加到文件“/lib/modules/3.0.0-16-generic.dep”中。使用depmod命令检测完依赖关系后,可调用modprobe命令“# modprobe word_count”装载Linux驱动。使用以上两命令需注意的有:depmod必须使用Linux驱动模块的绝对路径;depmod将内核模块的依赖信息写入当前正在使用的内核的modules.dep文件;modprobe只需使用驱动名称即可,不需要跟.ko。