《Essential Linux Device Drivers》第5章.docx
- 文档编号:4018109
- 上传时间:2022-11-27
- 格式:DOCX
- 页数:50
- 大小:54.01KB
《Essential Linux Device Drivers》第5章.docx
《《Essential Linux Device Drivers》第5章.docx》由会员分享,可在线阅读,更多相关《《Essential Linux Device Drivers》第5章.docx(50页珍藏版)》请在冰豆网上搜索。
《EssentialLinuxDeviceDrivers》第5章
第5章 字符设备驱动
本章内容
·字符设备驱动基础
·设备例子:
系统CMOS
·检测数据可获得
·和并口交互
·RTC 子系统
·伪字符驱动
·混杂驱动
·CharacterCaveats
·查看源代码
现在,你已经准备就绪了,可以尝试去写一个简单、但实用的设备驱动了。
在这一章,我们将深入字符设备驱动的内幕:
顺序存取设备数据的内核代码。
字符设备驱动能从如下几类设备获取原始的数据:
如打印机、鼠标、看门狗、键盘、内存、实时时钟等,但它不适合用于以块方式存储的、随机访问的设备,如硬盘、软盘和光盘。
·字符设备驱动基础
让我们以自顶向下的方式开始字符设备驱动学习之旅。
为了访问一个字符设备,系统用户需要调用相应的应用程序。
此应用程序负责和设备交互,为了实现此目的,需要得到相应驱动的标识符。
驱动通过/dev目录给用户提供接口:
bash>ls-l/dev
total0
crw-------1rootroot5,1Jul1610:
02console
...
lrwxrwxrwx1rootroot3Oct610:
02cdrom->hdc
...
brw-rw----1rootdisk3,0Oct62007hda
brw-rw----1rootdisk3,1Oct62007hda1
...
crw-------1roottty4,1Oct610:
20tty1
crw-------1roottty4,2Oct610:
02tty2
ls命令输出结果的每一行的第一个字符表示驱动的类型:
c表示字符设备驱动,b代表块设备驱动,l表示符号链接。
第五列的数字是主设备号,第六列是次设备号。
主设备号通常标识设备对应的驱动程序,次设备号用于确定驱动程序所服务的设备。
例如,IDE块存储设备驱动/dev/had的主设备号为3,负责处理系统的硬盘;当进一步指明其次设备号为1时(/dev/hda1),它指向第一个硬盘分区。
字符设备驱动与块设备驱动占用不同空间,因此可以将同一个主设备号分配给字符设备和块设备驱动。
让我们进一步深入字符设备驱动。
从程序结构的角度看,字符设备驱动包括如下内容:
--[if!
supportLists]-->
--[endif]-->初始化例程init(),负责初始化设备并且将驱动和内核的其它部分通过注册函数实现无缝连接。
--[if!
supportLists]-->
--[endif]-->入口函数(或方法)集,如open(),read(),ioctl(),llseek(),和write(),这些函数直接对应相应的的I/O系统调用,由用户应用程序通过对应的/dev节点调用。
--[if!
supportLists]-->
--[endif]-->中断例程,底半部例程,定时器处理例程,内核辅助线程,以及其他的组成部分。
它们大部分对用户应用程序是透明的。
从数据流的角度看,字符设备驱动包括如下关键的数据结构:
--[if!
supportLists]-->1.
--[endif]-->特定设备相关(pre-device)的数据结构。
此结构保存着驱动频繁使用的信息。
--[if!
supportLists]-->2.
--[endif]-->cdev结构,针对字符设备驱动的内核抽象。
这个结构通常作为前面讨论过的特定设备相关(pre-device)结构的成员。
--[if!
supportLists]-->3.
--[endif]-->file_operations结构,包括所有设备驱动入口函数的地址。
--[if!
supportLists]-->4.
--[endif]-->file结构,包括关联的/dev节点的信息。
·设备例子:
系统CMOS
让我们实现一个字符设备驱动以访问系统CMOS。
在PC兼容的硬件上(见图5.1)BIOS使用CMOS存储系统信息,如启动选项,引导顺序,系统数据等,我们可以通过BIOS设置菜单对其进行配置。
我们的CMOS设备驱动使你像访问普通文件一样访问两个PCCMOS块bank。
应用程序可以在/dev/cmos/0和/dev/cmos/1上使用I/O系统调用从两个块bank存取数据。
因为BIOS分配给CMOS域的存取粒度是比特级的,所以驱动程序能够进行比特级的访问。
因此,read()可以获取指定数目的比特,并根据读取的比特数移动内部文件指针。
图 5.1.PC兼容系统的CMOS
通过两个I/O地址访问CMOS,一个索引寄存器和一个数据寄存器,如表5.1所示。
你必须在索引寄存器中指定准备访问的CMOS存储器的偏移,然后通过数据寄存器来交换数据。
表 5.1.CMOS中寄存器分布
寄存器名称
描述
CMOS_BANK0_INDEX_PORT
在此寄存器中指定了CMOS块bank 0的偏移。
SpecifythedesiredCMOSbank0offsetinthisregister.
CMOS_BANK0_DATA_PORT
对CMOS_BANK0_INDEX_PORT指定的地址读取/写入数据。
CMOS_BANK1_INDEX_PORT
在此寄存器中指定了CMOS块bank1的偏移。
CMOS_BANK1_DATA_PORT
对CMOS_BANK0_INDEX_PORT指定的地址读取/写入数据。
因为每个驱动方法都有一个对应的由应用程序使用的系统调用,我们将看看系统调用和相应的驱动方法。
驱动初始化
init()函数是注册机制的基础。
它负责完成如下工作:
·申请分配主设备号。
·为特定设备相关(pre-device)的数据结构分配内存。
·将入口函数(open(),read()等)和字符驱动的cdev抽象相关联。
·将主设备号和驱动的cdev相关联。
·在/dev 和 /sys下创建节点。
如在第4章“打下基础”中所讨论的,/dev的管理经历了2.2版本内核中的静态设备节点,到2.4中的动态指定name, 再到2.6中的用户空间的守护进程(udevd)的历程。
·初始化硬件。
在本例的简单CMOS驱动中不涉及此部分。
清单5.1实现了CMOS驱动的init()函数。
清单5.1.CMOS 驱动初始化
代码
#include
/*Per-device(per-bank)structure*/
structcmos_dev{
unsignedshortcurrent_pointer;/*Currentpointerwithinthe
bank*/
unsignedintsize;/*Sizeofthebank*/
intbank_number;/*CMOSbanknumber*/
structcdevcdev;/*Thecdevstructure*/
charname[10];/*NameofI/Oregion*/
/*...*//*Mutexes,spinlocks,wait
queues,..*/
}*cmos_devp;
/*Fileoperationsstructure.Definedinlinux/fs.h*/
staticstructfile_operationscmos_fops={
.owner=THIS_MODULE,/*Owner*/
.open=cmos_open,/*Openmethod*/
.release=cmos_release,/*Releasemethod*/
.read=cmos_read,/*Readmethod*/
.write=cmos_write,/*Writemethod*/
.llseek=cmos_llseek,/*Seekmethod*/
.ioctl=cmos_ioctl,/*Ioctlmethod*/
};
staticdev_tcmos_dev_number;/*Allotteddevicenumber*/
structclass*cmos_class;/*Tiewiththedevicemodel*/
#defineNUM_CMOS_BANKS2
#defineCMOS_BANK_SIZE(0xFF*8)
#defineDEVICE_NAME"cmos"
#defineCMOS_BANK0_INDEX_PORT0x70
#defineCMOS_BANK0_DATA_PORT0x71
#defineCMOS_BANK1_INDEX_PORT0x72
#defineCMOS_BANK1_DATA_PORT0x73
unsignedcharaddrports[NUM_CMOS_BANKS]={CMOS_BANK0_INDEX_PORT,
CMOS_BANK1_INDEX_PORT,};
unsignedchardataports[NUM_CMOS_BANKS]={CMOS_BANK0_DATA_PORT,
CMOS_BANK1_DATA_PORT,};
/*
*DriverInitialization
*/
int__init
cmos_init(void)
{
inti;
/*Requestdynamicallocationofadevicemajornumber*/
if(alloc_chrdev_region(&cmos_dev_number,0,
NUM_CMOS_BANKS,DEVICE_NAME)<0){
printk(KERN_DEBUG"Can'tregisterdevice\n");return-1;
}
/*Populatesysfsentries*/
cmos_class=class_create(THIS_MODULE,DEVICE_NAME);
for(i=0;i /*Allocatememoryfortheper-devicestructure*/ cmos_devp=kmalloc(sizeof(structcmos_dev),GFP_KERNEL); if(! cmos_devp){ printk("BadKmalloc\n");return1; } /*RequestI/Oregion*/ sprintf(cmos_devp->name,"cmos%d",i); if(! (request_region(addrports[i],2,cmos_devp->name)){ printk("cmos: I/Oport0x%xisnotfree.\n",addrports[i]); return–EIO; } /*Fillinthebanknumbertocorrelatethisdevice withthecorrespondingCMOSbank*/ cmos_devp->bank_number=i; /*Connectthefileoperationswiththecdev*/ cdev_init(&cmos_devp->cdev,&cmos_fops); cmos_devp->cdev.owner=THIS_MODULE; /*Connectthemajor/minornumbertothecdev*/ if(cdev_add(&cmos_devp->cdev,(dev_number+i),1)){ printk("Badcdev\n"); return1; } /*Sendueventstoudev,soit'llcreate/devnodes*/ class_device_create(cmos_class,NULL,(dev_number+i), NULL,"cmos%d",i); } printk("CMOSDriverInitialized.\n"); return0; } /*DriverExit*/ void__exit cmos_cleanup(void) { inti; /*Removethecdev*/ cdev_del(&cmos_devp->cdev); /*Releasethemajornumber*/ unregister_chrdev_region(MAJOR(dev_number),NUM_CMOS_BANKS); /*ReleaseI/Oregion*/ for(i=0;i class_device_destroy(cmos_class,MKDEV(MAJOR(dev_number),i)); release_region(addrports[i],2); } /*Destroycmos_class*/ class_destroy(cmos_class); return(); } module_init(cmos_init); module_exit(cmos_cleanup); cmos_init()完成的大部分工作是通用的,因此如果替换掉和CMOS数据结构相关的部分,你可以使用清单5.1作为模板开发其他的字符设备驱动。 首先,cmos_init()调用alloc_chrdev_region(),动态申请一个未用的主设备号。 如果调用成功,dev_number包含系统分配的主设备号。 alloc_chrdev_region()的第二个参数是请求的起始次设备号,第三个参数是支持的次设备号数目。 最后一个参数是和CMOS关联的设备名称,它将出现在/proc/devices中: bash>cat/proc/devices|grepcmos 253cmos 253是动态分配给CMOS设备的主设备号。 在2.6内核以前,不支持动态设备节点分配,因此字符驱动使用register_chrdev()调用去静态申请指定的主设备号。 继续分析代码之前,让我们先快速浏览一下清单5.1中所使用的数据结构。 cmos_dev就是和特定设备相关(per-device)的数据结构。 cmos_fops是file_operations类型的数据结构,它包含驱动入口地址。 cmos_fops还包括一个owner域,其值被置为THIS_MODULE,也就是正讨论的驱动模块的地址。 在处理设备的打开和释放时,知晓结构拥有者的身份可以让内核帮助驱动减轻一些事务管理的负担,如跟踪使用计数。 正如你所看到的,内核用cdev结构来表示字符设备。 字符驱动通常将cdev结构包含于字符设备特定的数据结构per-device中。 在我们的例子中,cdev定义于cmos_dev中。 cmos_init()为本驱动程序服务的每一个设备(在本例子中是CMOSbank)分配相关数据结构的内存: 包括设备特定(pei-device)的数据结构、及其cdev结构。 cev_init()将文件操作(cmos_fops)和cdev关联,cdev_add()将通过alloc_chrdev_region()分配的主/次设备号和cdev连接在一起。 class_create() 为此设备构建了sysfs入口点,class_device_create()导致了两个uevent的产生: cmos0和cmos1。 正如你在第四章中所学习的,udevd监听uevent,在查阅规则数据库后创建设备节点。 为了在接收到相应的uevent(cmos0和cmos1)后,创建和两个CMOS 块bank(/dev/cmos/0和/dev/cmos/1)对应的设备节点,需要将如下内容添加进udev规则目录中(/etc/udev/rules.d/): KERNEL="cmos[0-1]*",NAME="cmos/%n" 通过调用request_region()申请I/O地址后,设备驱动在申请的I/O地址中操作运行。 此机制确保其他程序不能申请此区域,直至通过调用release_region()释放占用的区域。 request_region()通常被I/O总线驱动程序调用,例如PCI和ISA,以指明对于处理器地址空间中on-card内存的拥有权(更多细节请见第十章,“PCI总线”)。 cmos_init()调用request_region()去申请每个CMOSbank的I/O。 request_region()的最后一个参数是一个被/proc/ioports使用的标识符,如果你查看文件/proc/ioports将会看到: bash>grepcmos/proc/ioports 0070-0071: cmos0 0072-0073: cmos1 完成注册过程后,cmos_init()打印一条信息,宣告其一切顺利! Open 与 Release 当应用程序打开设备节点时,内核调用相应驱动的open()函数。 你可以在shell中执行: bash>cat/dev/cmos/0 来触发cmos_open()的执行。 当应用程序关闭一个已经打开的设备时,内核调用release()函数。 因此当读取CMOSbank0的内容后,cat关闭和/dev/cmos/0关联的文件描述符,内核调用cmos_release()。 清单5.2显示了cmos_open()和cmos_release()的具体实现。 让我们近距离接触cmos_open()。 有两点值得我们关注。 首先是cmos_dev的获取。 cmos_open()的输入参数inode包含cdev结构在初始化过程中分配的地址。 正如清单5.1中所显示的,cdev定义于cmos_dev中。 为了获取容器结构coms_dev的地址,cmos_open()使用了内核的辅助函数container_of()。 cmos_open()中另外值得关注的是第二个输入参数、file结构中private_data数据域的使用。 你可以使用此数据域(file->private_data)作为占位符,方便的关联到其它驱动方法。 CMOS驱动程序中使用此数据域来存储cmos_dev的地址。 阅读cmos_release()(以及函数的其他部分)可以发现private_data被如何用来直接获取属于相应CMOS块bank的cmos_dev结构体的处理程序。 清单5.2.Open与Release 代码 /* *OpenCMOSbank */ int cmos_open(structinode*inode,structfile*file) { structcmos_dev*cmos_devp; /*Gettheper-devicestructurethatcontainsthiscdev*/ cmos_devp=container_of(inode->i_cdev,structcmos_dev,cdev); /*Easyaccesstocmos_devpfromrestoftheentrypoints*/ file->private_data=cmos_devp; /*Initializesomefields*/ cmos_devp->size=CMOS_BANK_SIZE; cmos_devp->current_pointer=0; return0; } /* *ReleaseCMOSbank */ int cmos_release(structinode*inode,structfile*file) { structcmos_dev*cmos_devp=file->private_data; /*Resetfilepointer*/ cmos_devp->current_pointer=0; return0; } 数据交换 read()和write()是负责在用户空间和设备之间交换数据的主要的字符驱动函数。 扩展的read()/write()函数族包括几个其他的函数: fsync(),aio_read(),aio_write(),和mmap()。 CMOS驱动在一个简单的存储设备上操作,而不必处理其他一般的字符驱动必须面对的复杂问题。 · --[if! supportLists]--> --[endif]-->CMOS数据访问例程不必“休眠——等待”设备I/O完成,其他字符驱动的read()和write()则必须支持阻塞和非阻塞操作模式。 除非设备文件在非阻塞方式(O_NONBLOCK)下打开,否则read()和write()将会使调用进程休眠至I/O操作完成。 · --[if! supportLists]--> --[endif]-->CMOS驱动操作基于同步方式,不必依赖中断。 然而,很多驱动的数据访问函数依靠中断来获取数据,并且需要通过等待队列等数据结构和中断上下文代码通信。 清单5.3包括CMOS驱动的read()和write()函数。 你不能从内核中直接访问用户空间的缓冲区,反之亦然。 因此为了拷贝CMOS存储器数据至用户空间,cmos_read()需要调用copy_to_user()。 cmos_write()通过调用copy_from_user()完成相反的工作。 因为copy_to_user()和copy_from_user()可能会睡眠,所以在调用这两个函数的时候不能持有自选锁。 如同你在前面所看到的,通过操作一对I/O地址,完成CMOS内存访问。 为了从I/O地址中读取不同大小的数据,内核提供了一组和结构无关的函数: in[b|w|l|sb|sl]()。 类似地,可以通过out[b|w|l|sb|sl]()去写I/O区域。 清单5.3中port_data_in()和port_data_out()使用inb()和outb()进行数据传输。 清单 5.3.ReadandWrite 代码 /* *ReadfromaCMOSBankatbit-levelgranularity */ ssize_t cmos_read(structfile*file,char*buf, size_tcount,loff_t*ppos) { structcmos_dev*cmos_devp=file->priv
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Essential Linux Device Drivers Essential Linux Device Drivers第5章 Essential Drivers
![提示](https://static.bdocx.com/images/bang_tan.gif)