一头雾水的API函数刚开始接触Linux驱动的时候,我很苦恼:注册一个字符设备,怎么会有那么多API函数?在每篇参考文章中,使用的函数都不同。但执行结果符合预期!例如,以下内容:register_chrdev(...);register_chrdev_regin(...);cdev_add(...);它们的作用都是向系统注册字符设备,但是光看函数名,初学者谁能分得清呢?!难怪Linux系统发展了这么多年,代码更新是很正常的事情。然而,我们参考的文章却做不到:文章中描述的内容的背景介绍很详细,而文章作者往往是通过在自己的实际工作环境中测试某种方法来解决自己的问题,所以记录一下吧。不同的文章,不同的工作语境,不同的API函数调用,往往让我们初学者吃尽苦头,尤其是像我这样有选择障碍的人!其实上面的功能都是正确的,它们的功能也都差不多,都是Linux系统不同阶段的产物。老API函数在Linux内核代码2.4和2.6早期版本中,注册和卸载字符设备驱动的经典方式是:注册设备:intregister_chrdev(unsignedintmajor,constchar*name,structfile_operations*fops);参数1major:如果为0-操作系统动态分配给该设备的主设备号;如果它不是0-驱动程序应用到系统并使用这个主设备号;参数2name:设备名称;参数3fops:file_operations类型的指针变量,用于操作设备;如果是动态分配的,则该函数的返回值为:操作系统动态分配给该设备的主设备号。我们需要记住这个动态分配的设备号,因为它需要在其他API函数中使用。卸载设备:intunregister_chrdev(unsignedintmajor,constchar*name)参数1major:设备的主设备号,即register_chrdev()函数的返回值(动态),或者驱动指定的设备号(静态方式));参数2name:设备名称;新的API函数注册设备:intregister_chrdev_region(dev_tfrom,unsignedcount,constchar*name);intalloc_chrdev_region(dev_t*dev,unsignedbaseminor,unsignedcount,constchar*name);上面两个注册设备函数其实对应的是老API函数register_chrdev:将参数1表示的动态分配和静态分配拆分成两个函数。也就是说:register_chrdev_region():静态注册设备;alloc_chrdev_region():动态注册设备;这两个函数的参数含义为:register_chrdev_region参数:参数1from:注册指定设备号,静态指定,例如:MKDEV(200,0)表示起始主设备号为200,起始次设备号为0;参数2count:驱动指定连续注册的次设备号的个数,例如:起始次设备号为0,count如果为10,则表示驱动将使用从0到9的10个次设备号;参数3name:设备名称;alloc_chrdev_region参数:参数1dev:动态注册是指系统分配设备号,驱动必须提供一个指针变量来接收系统分配的结果(设备号);参数2baseminor:驱动指定这个设备号的起始值;参数3count:驱动指定连续注册的次设备号的个数,例如:起始次设备号为0,count为10,表示驱动会使用0到9的10个子设备号;参数4name:设备名称;添加关于设备号的内容:这里的结构体dev_t用于保存设备号,包括主设备号和次设备号。它本质上是一个32位的数字,其中12位用于表示主编号,其余20位用于表示次编号。系统定义了三个宏来实现dev_t变量、主设备号、次设备号之间的转换:MAJOR(dev_tdev):从dev_t类型中获取主设备号;MINOR(dev_tdev):从dev_t类型中得到Minor设备号;MKDEV(intmajor,intminor):将主次设备号转换为dev_t类型;卸载设备:voidunregister_chrdev_region(dev_tfrom,unsignedcount);参数1from:无符号设备号;参数2count:unsigned连续的子设备号的个数;下面实际的代码操作,我们将使用旧的API函数来一步步描述字符设备驱动程序:写入、加载和卸载的过程。如何使用新的API函数编写字符设备驱动程序将在下一篇文章中详细讨论。以下所有操作的工作目录与上一篇相同,即:~/tmp/linux-4.15/drivers/。创建驱动目录和驱动$cdlinux-4.15/drivers/$mkdirmy_driver1$cdmy_driver1$touchdriver1.cdriver1.c文件内容如下(不用手动敲,文末有代码下载链接):#include#include#include#include#include#include#include#include#include#includestaticunsignedintmajor;intdriver1_open(structinode*inode,structfile*file){printk("driver1_openiscalled.\n");return0;}ssize_tdriver1_read(structfile*file,char__user*buf,size_tsize,loff_t*ppos){printk("driver1_readiscalled.\n");return0;}ssize_tdriver1_write(structfile*file,constchar__user*buf,size_tsize,loff_t*ppos){printk("driver1_writeiscalled.\n");return0;}staticconststructfile_operationsdriver1_ops={.owner=THIS_MODULE,.open=driver1_open,.read=driver1_read,.write=driver1_write,};staticint__initdriver1_init(void){printk("driver1_initiscalled.\n");major=register_chrdev(0,"driver1",&driver1_ops);printk("register_chrdev.major=%d\n",major);return0;}staticvoid__exitdriver1_exit(void){printk("driver1_exitiscalled.\n");unregister_chrdev(major,"driver1");}MODULE_LICENSE("GPL");module_init(driver1_init);module_exit(driver1_exit);创建Makefile$touchMakefile内容如下:ifneq($(KERNELRELEASE),)obj-m:=driver1.oelseKERNELDIR?=/lib/modules/$(shelluname-r)/buildPWD:=$(shellpwd)默认:$(MAKE)-C$(KERNELDIR)M=$(PWD)modulesclean:$(MAKE)-C$(KERNEL_PATH)M=$(PWD)cleanendif编译驱动模块$make得到驱动程序:driver1.ko载入驱动模块在载入驱动模块之前,首先看一下与驱动设备相关的几个地方系统。首先查看/dev目录,还没有设备节点(/dev/driver1)。然后查看/proc/devices目录,没有driver1设备的设备号。cat/proc/devices|grepdriver1/proc/设备文件:列出主要的字符设备和块设备的设备编号,以及分配给这些设备编号的设备名称。执行以下命令加载驱动模块:$sudoinsmoddriver1.ko从上一篇文章我们知道,驱动加载时,通过module_init(driver1_init)注册的函数driver1_init();将被执行,并输出打印信息。或者使用dmesg命令查看驱动模块的打印信息:$dmesg如果输入的信息过多,可以使用dmesg|尾部命令;这个时候,驱动模块已经加载完毕!查看/proc/devices目录下显示的设备号:可以看到driver1已经挂载,其主设备号为244。此时,虽然设备已经注册到系统中,主设备号已经已经分配好了,/dev目录下没有这个设备的节点,所以我们需要手动创建:sudomknod-m660/dev/driver1c2440查看设备节点是否创建成功:$ls-l/dev关于设备节点,Linux应用层有udev服务,可以自动创建设备节点;即:驱动模块加载时,会自动在/dev目录节点创建设备。当然,我们需要在driver中提前告诉udev如何创建;下面将介绍:如何自动创建设备节点。现在,设备驱动已经加载,设备节点已经创建,应用程序可以操作(读、写)设备了。应用程序我们将所有应用程序放在~/tmp/App/目录中。$cd~/tmp$mkdir-pApp/app_driver1$touchapp_driver1.capp_driver1.c文件内容如下:#include#include#includeintmain(void){内推;intread_data[4]={0};intwrite_data[4]={1,2,3,4};intfd=open("/dev/driver1",O_RDWR);if(-1!=fd){ret=read(fd,read_data,4);printf("readret=%d\n",ret);ret=write(fd,write_data,4);printf("writeret=%d\n",ret);}else{printf("open/dev/driver1failed!\n");}return0;}这里的演示只是通过打印信息来体现函数调用,并不真正读写数据。因为读写数据涉及用户空间和内核空间之间复杂的数据拷贝问题。应用准备好了,接下来就是编译测试了:$gccapp_driver1.c-oapp_driver1$sudo./app_driver1应用输出如下:app_driver1$sudo./app_driver1[sudo]passwordforxxxx:readret=0writeret=0从返回值来看,设备打开成功,read函数和write函数调用成功!根据Linux系统的驱动框架,当调用应用层的open、read、write函数时,会执行相应的函数:staticconststructfile_operationsdriver1_ops={.owner=THIS_MODULE,.open=driver1_open,.read=driver1_read,.write=driver1_write,};我们在驱动的这三个函数中打印了信息,继续使用dmesg命令查看:卸载驱动模块卸载命令:$sudormmoddriver1继续使用dmesg命令查看驱动中的打印信息:意思是调用驱动程序中的driver1_exit()函数。此时我们再看一下/proc/devices目录的变化:可以看到设备号为244的driver1已经被系统卸载了!因为unregister_chrdev(major,"driver1");驱动程序中的功能已被执行。但是由于刚才/dev目录下的设备节点driver1是手动创建的,所以我们需要手动删除它。$sudorm/dev/driver1总结以上就是最简单的字符设备驱动!从编写过程可以看出,Linux系统已经设计了一套驱动框架。我们只需要把每个功能或结构按其要求一步步注册到系统中即可。在/dev目录下自动创建设备节点在上面的操作过程中,手动创建了设备节点/dev/driver1。Linux系统的应用层提供了udev服务,可以帮助我们自动创建设备节点。让我们现在添加此功能。修改驱动程序。为了方便对比,所有添加的代码都由宏定义UDEV_ENABLE控制。在driver1.c代码中,有3个变化:1.定义2个全局变量\n");major=register_chrdev(0,"driver1",&driver1_ops);printk("register_chrdev.major=%d\n",major);#ifdefUDEV_ENABLEdriver1_class=class_create(THIS_MODULE,"driver1");driver1_dev=device_create(driver1_class,NULL,MKDEV(major,0),NULL,"driver1");#endifreturn0;}3.driver1_exit()函数staticvoid__exitdriver1_exit(void){printk("driver1_exit被调用。\n");#ifdefUDEV_ENABLEclass_destroy(driver1_class);#endifunregister_chrdev(major,"driver1");}代码修改后(也可以直接下载我放在网盘的源码),重新编译驱动模块:$make生成driver1.ko驱动模块,然后加载它:首先确定:/proc/devices,/dev目录下没有刚测试过的设备;为了查看驱动中的打印信息,最好将dmesg输出的打印信息清理干净点击(命令:sudodmesg-c);$sudoinsmoddriver1.ko按照刚才的操作流程,我们需要验证3个信息:(1)查看驱动的打印信息(命令:dmesg):图(2)查看/proc/devices下设备注册状态:图(3)查看/dev下是否自动创建设备节点:图片通过以上3一张图,可以得出结论:驱动加载正确,设备节点自动创建!接下来应该是应用程序登场测试了,代码不用修改,直接执行即可:$sudo./app_driver1[sudo]passwordforxxx:readret=0writeret=0的应用层函数返回值正确!再看看dmesg的输出信息:完美!代码下载文中所有代码均已放入网盘