第十三章--I/O体系结构和设备驱动程序

一、I/O体系结构
        为了确保计算机能够正常工作,必须提供数据通路,让信息在连接到个人计算机的CPU、RAM和I/O设备之间流动。这些数据通路总称为总线,担当计算机内部主通信通道的作用。
        一台计算机包括几种不同类型的总线,它们通过被称作“桥”的硬件设备连接在一起。两条高速总线用于在内存芯片上来回传送数据:前端总线将CPU连接到RAM控制器上,而后端总线将CPU直接连接到外部硬件的高速缓存上。主机上的桥将系统总线和前端总线连接在一起。
        任何I/O设备有且仅能连接一条总线。总线的类型影响I/O设备的内部设计,也影响着内核如何处理设备。
        CPU和I/O设备之间的数据通路通常称为I/O总线。
1.1、I/O端口
        每个连接到I/O总线上的设备都有自己的I/O地址集,通常称为I/O端口(I/O port)。可以把两个连续的8位端口看成一个16位端口,但是这必须从偶数地址开始。同理,也可以把两个连续的16位端口看成一个32位端口,但是这必须是从4的整数倍地址开始。有四条专用的汇编语言指令可以允许CPU对I/O端口进行读写,它们是in、ins、out和outs。在执行其中的一条指令时,CPU使用地址总线选择所请求的I/O端口,使用数据总线在CPU寄存器和端口之间传送数据。
        I/O端口还可以被映射到物理地址空间。因此,处理器和I/O设备之间的通信就可以使用对内存直接进行操作的汇编语言指令(例如,mov、and、or等等)。现代的硬件设备更倾向于映射的I/O,因为这样处理的速度较快,并可以和DMA结合起来。
1.1.1、访问I/O端口
        内核必须使用“资源”来记录分配给每个硬件设备的I/O端口。
        资源(resource)表示某个实体的一部分,这部分被互斥地分配给设备驱动程序。在我们的情况中,一个资源表示I/O端口地址的一个范围。每个资源对应的信息存放在resource数据结构中。所有的同种资源都插入到一个树形数据结构中;例如,表示I/O端口地址范围的所有资源都包含在一个根节点ioport_resource的树中。
        内核也为以上应用于I/O端口的函数定义了一些快捷函数:request_region()分配I/O端口的给定范围,release_region()释放以前分配给I/O端口的范围。当前分配给I/O设备的所有I/O地址的树都可以从/proc/ioports文件中获得。
1.2、I/O接口
        I/O接口(I/O interface)是处于一组I/O端口和对应的设备控制器之间的一种硬件电路。它起翻译器的作用,即把I/O端口中的值转换成设备所需要的命令和数据。在相反的方向上,它检测设备状态的变化,并对起状态寄存器作用的I/O端口进行相应的更新。还可以通过一条IRQ线把这种电路连接到可编程中断控制器上,以使它代表相应的设备发出中断请求。
        有两种类型的接口:
        专用I/O接口:专门用于一个特定的硬件设备。在一些情况下,设备控制器与这种I/O接口处于同一块卡中。连接到专用I/O接口上的设备可以是内部设备,也可以是外部设备。
        通用I/O接口:用来连接多个不同的硬件设备。连接到通用I/O接口上的设备通常都是外部设备。
1.2.1、专用I/O接口

1.2.2、通用I/O接口

1.3、设备控制器
        复杂的设备可能需要一个设备控制器(device controller)来驱动。从本质上说,控制器起两个重要作用:
        * 对从I/O接口接收到的高级命令进行解释,并通过向设备发送适当的电信号序列强制设备执行特定的操作。
        * 对从设备接收到的电信号进行转换和适当地解释,并修改(通过I/O接口)状态寄存器的值。
二、设备驱动程序模型
        新的硬件设备即使类型不同但也有相似的功能。对这种设备的驱动程序应当特别关注:
        * 电源管理(控制设备电源线上不同的电压级别)。
        * 即插即用(配备设备时透明的资源分配)。
        * 热插拔(系统运行时支持设备的插入和移走)。
        系统中所有硬件设备由内核全权负责电源管理。例如,在以电池供电的计算机进入“待机”状态时,内核应立刻强制每个硬件设备(硬盘、显卡、声卡、网卡、总线控制起等等)处于低功率状态。因此,每个能够响应“待机”状态的设备驱动程序必须按准确的顺序进入“待机”状态。否则一些设备可能会处于错误的电源状态。例如,内核必须首先将硬盘置于“待机”状态,然后才是它们的硬盘控制器,因为若按照相反的顺序执行,硬盘控制器就不能向硬盘发送命令。
2.1、sysfs文件系统
        sysfs文件系统是一种特殊的文件系统,被安装于/sys目录下的/proc文件系统相似。/proc文件系统是首次被设计成允许用户态应用程序访问内核内部数据结构的一种文件系统。/sysfs文件系统本质上与/proc有相同的目的,但是它还提供关于内核数据结构的附加信息;此外,/sysfs的组织结构比/proc更有调理。或许,在不远的将来,/proc和/sysfs将会继续共存。
        sysfs文件系统的目标是要展现设备驱动程序模型组件间的层次关系。该文件系统的相应高层目录是:
        block:块设备,它们独立于所连接的总线。
        devices:所有被内核识别的硬件设备,依照连接它们的总线对其进行组织。
        bus:系统中用于连接设备的总线。
        drivers:在内核中注册的设备驱动程序。
        class:系统中设备的类型;同一类可能包含由不同总线连接的设备,于是由不同的驱动程序驱动。
        power:处理一些硬件设备电源状态的文件。
        firmware:处理一些硬件设备的固件的文件。
2.2、kobject
        kobject被嵌入一个叫做“容器”的更大对象中,容器描述设备驱动程序模型中的组件。容器的典型例子有总线、设备以及驱动程序的描述符;例如,第一个IDE磁盘的第一个分区描述符对应于/sys/block/hda/hdal目录。
        将一个kobject嵌入容器中允许内核:
        * 为容器保持一个引用计数器。
        * 维持容器的层次列表或组(例如,与块设备相关的sysfs目录为每个磁盘分区包含一个不同的子目录)。
        * 为容器的属性提供一种用户态查看的视图。
2.2.1、kobject、kse和subsystem
        通过kset数据结构可将kobjects组织成一颗层次树。kset是同类型kobject结构的一个结合体----也就是说,相关的kobject包含在同类型的容器中。
        kobj字段是嵌入在Kset数据结构中的kobject;而位于kset中的kobject,其parent字段指向这个内嵌的kobject结构。因此,一个kset就是kobject集合体,但是它依赖于层次树中用于引用计数和连接的更高层kobject。这种设计编码效率很高,并可获得最大的灵活性。例如,分别用于增加和减少kset引用计数器值的kset_get()函数和kset_put()函数,只需简单地调用内嵌的kobject结构中的kobject_get()函数和kobject_put()函数;因为kset的引用计数器只不过是内嵌的Kse中的类型为kobject的kobj的引用计数器。而且,由于有了内嵌的kobject结构,kset数据结构可以嵌入到“容器”对象中,非常类似于嵌入的kobject数据结构。最后,kset可以作为其他kset的一个成员:它足以将内嵌的kobject插入到更高层次的kset中。
        还存在所谓subsystem的kset集合。一个subsystem可以包括不同类型的kset,用包含两个字段的subsystem数据结构来描述:
        kset:内嵌的kset结构,用于存放subsystem中的kset。
        rwsem:读写信号量,保护递归地包含于subsystem中的所有kset和kobject。
        subsystem数据结构甚至也可以嵌入到一个更大的“容器”对象中;因此,容器的应用计数器也是内嵌subsystem的引用计数器----也就是嵌入在subsystem中的kset所嵌的kobject的引用计数器。subsys_get()和subsys_put()函数分别用于增加和减少这个引用计数器的值。
2.2.2、注册kobject、kset和subsystem
        一般来说,如果想让kobject、kset和subsystem出现在sysfs子树中,就必须首先注册它们。与kobject对应的目录总是出现在其父kobject的目录中。例如,位于同一个kset中的kobject的目录出现在kset本身的目录中。因此,sysfs子树的结构就描述了各种已注册的kobject之间以及各种容器对象之间的层次关系。
        通常,sysfs文件系统的上层目录肯定是已注册的subsystem。
        kobject_register()函数用于初始化kobject,并且将其相应的目录增加到sysfs文件系统中。在调用此函数之前,调用程序应该先设置kobject结构中的kset字段,使它指向其父kset(如果有的话)。kobject_unregister()函数则将kobect的目录从sysfs文件系统中移走。为了更易于内核开发者进行开发,Linux也提供了kset_register()和kset_unregister()函数,以及subsystem_register() subsystem_unregister()函数,但本质上它们是围绕kobject_register()和kobject_unregister()的封装函数。
        许多kobject目录都包括称作属性(attribute)的普通文件。sysfs_create_file()函数接收kobject的地址和属性描述符作为它的参数,并在合适的目录中创建特殊文件。sysfs文件系统中所描述的对象间的其他关系可以通过符号链接的方式来建立:sysfs_create_link()函数为目录中与其他kobject相关联的特定kobject创建一个符号链接。
2.3、设备驱动程序模型的组件
        设备驱动程序模型建立在几个基本数据结构之上,这些结构描述了总线、设备、设备驱动器等等。
2.3.1、设备
        设备驱动程序模型中的每个设备是由一个device对象来描述的。
        device对象全部收集在devices_subsys子系统中,该子系统对应的目录为/sys/devices。设备是按照层次关系组织的:一个设备是某个“孩子”的“父亲”,其条件为子设备离开父设备无法正常工作。例如,在基于PCI总线的计算机上,位于PCI总线和USB总线之间的桥就是连接在USB总线上的所有设备的父设备。device对象的parent字段是指向其父设备描述符的指针,children字段是子设备链表的首部,而node字段存放指向children链表中相邻元素的指针。device对象中内嵌的kobject间亲子关系也反映了设备的层次关系;因此,/sys/devices下的目录结构与硬件设备的物理组织是匹配的。
        每个设备驱动程序都保持一个device对象链表,其中链接了所有可被管理的设备;device对象的driver_list字段存放指向相邻对象的指针,而driver字段指向设备驱动程序的描述符。此外,对于任何总线类型来说,都有一个链表存放连接到该类型总线上的所有设备;device对象的bus_list字段存放指向相邻对象的指针,而bus字段指向总线类型描述符。
        引用计数器记录device对象的使用情况,它包含在kobject类型的kobj结构中,通过调用get_device()和put_device()函数分别增加和减少该计数器的值。
        device_register()函数的功能是往设备驱动程序模型中插入一个新的device对象,并自动地在/sys/devices目录下为其创建一个新的目录。相反地,device_unregister()函数的功能是从设备驱动程序模型中移走一个设备。
        通常,device对象被静态地嵌入到一个更大的描述符中。
2.3.2、驱动程序
        设备驱动程序模型中的每个驱动程序都可由device_driver对象描述。
        device_driver对象包括四个方法,它们用于热插拔、即插即用和电源管理。当总线设备驱动程序发现一个可能由它处理的设备时就会调用probe方法;相应的函数将会探测硬件,从而对设备进行更一步的检查。当移走一个可热插拔的设备时驱动程序会调用remove方法;而驱动程序本身被卸载时,它所处理的每个设备也会调用remove方法。当内核必须改变设备的供电状态时,设备会调用shutdown、suspend和resume三个方法。
        内嵌在描述符中的kobject类型的kobj所包含的引用计数器用于记录device_driver对象的使用情况。通过调用get_drvier()函数和put_driver()函数可分别增加和减少该计数器的值。
        driver_register()函数的功能是往设备驱动程序模型中插入一个新的device_driver对象,并自动地在sysfs文件系统下为其创建一个新的目录。相反,driver_unregister()函数的功能则是从设备驱动程序模型中移走一个设备驱动对象。
2.3.3、总线
        内核所支持的每一种总线类型都由一个bus_type对象描述。
        每个bus_type类型的对象都包含一个内嵌的子系统;存放于bus_subsys变量中的子系统把嵌入在bus_type对象中的所有子系统都集合在一起。bus_subsys子系统与目录/sys/bus是对应的;因此,例如,有一个/sys/bus/pci目录,它与PCI总线类型相对应。每种总线的子系统通常包括两个kset,它们是drivers和devices(分别对应于bus_type对象中的drivers和devices字段)。
        名为driver的kset包含描述符device_driver,它描述与该总线类型相关的所有设备驱动程序,而名为device的kset包含描述符device,它描述给定总线类型上连接的所有设备。因为设备的kobject目录已经出现在/sys/devices下的sysfs文件系统中,所以每种总线子系统的devices目录存放了指向/sys/devices下目录的符号链接。bus_for_each_drv()和bus_for_each_dev()函数分别用于循环扫描drivers和devices链表中的所有元素。
        当内核检查一个给定的设备是否可以由给定的驱动程序处理时,就会执行match方法。对于连接设备的总线而言,即使其上每个设备的标识符都拥有一个特定的格式,实现match方法的函数通常也很简单,因为它只需要在所支持标识符的驱动程序表中搜索设备的描述符。在设备驱动程序模型中注册某个设备时会执行hotplug方法;实现函数应该通过环境变量把总线的具体信息传递给用户态程序,以通告一个新的可用设备。最后,当特定类型总线上的设备必须改变其供电状态时,就会执行suspend和resume方法。
2.3.4、类
        每个类是由一个class对象描述的。所有的类对象都属于与/sys/class目录相对应的class_subsys子系统。此外,每个类对象还包括一个内嵌的子系统;因此,例如有一个/sys/class/input目录,它就与设备驱动程序模型的input类相对应。
        设备驱动程序模型中的类本质上是要提供一个标准的方法,从而为用户态应用程序导出逻辑设备的接口。每个class_device描述符中内嵌一个kobject,这是一个名为dev的属性(特殊文件)。该属性存放设备的主设备号和次设备号,通过它们可以访问相应的逻辑设备。
三、设备文件
        我们至少可以假定以下的差异:
        * 块设备的数据可以被随机访问,而且从人类用户的观点看,传送任何数据块所需的时间都是较少且大致相同的。
        * 字符设备的数据或者不可以被随机访问,或者可以被随机访问,但是访问随机数据所需的时间很大程度上依赖于数据在设备内的位置。
        传统上,设备标识符由设备文件的类型(字符或块)和一对参数组成。第一个参数称为主设备号(major number),它标识了设备的类型。通常,具有相同主设备号和类型的所有设备文件共享相同的文件操作集合,因为它们是由同一个设备驱动程序处理的。第二个参数称为次设备号(minor number),它标识了主设备号相同的设备组中的一个特定设备。例如,由相同的磁盘控制器管理的一组磁盘具有相同的主设备号和不同的次设备号。
3.1、设备文件的用户态处理
        Linux2.6已经增加了设备号的编码大小:目前主设备号的编码为12位,次设备号的编码为20位。通常把这两个参数合并成一个32位的dev_t变量。MAJOR宏和MINOR宏可以从dev_t中分别提取主设备号和次设备号,而MKDEV宏可以把主设备号和次设备号合并成一个dev_t值。为了实现向后兼容,内核仍然可以正确地处理设备号编码为16位的老式设备文件。
3.1.1、动态分配设备号
        设备驱动程序模型提供了一个非常好的解决办法:把主设备号和次设备号存放在/sys/class子目录下的dev属性中。
3.1.2、动态创建设备文件
        Linux内核可以动态地创建设备文件:它无需把每一个可能想到的硬件设备的设备文件都填充到/dev目录下,因为设备文件可以按照需要来创建。由于设备驱动程序模型的存在,Linux2.6内核提供了一个非常简单的方法来处理这个问题。系统中必须安装一组称为udev工具集的用户态程序。当系统启动时,/dev目录是清空的,这时udev程序将扫描/sys/class子目录来寻找dev文件。对每一个这样的文件(主设备号和次设备号的组合表示一个内核所支持的逻辑设备文件),udev程序都会在/dev目录下为它创建一个相应的设备文件。udev程序也会根据配置文件为其分配一个文件名并创建一个符号链接,该方法类似于Unix设备文件的传统命令模式。最后,/dev目录里只存放了系统中内核所支持的所有设备的设备文件,而没有任何其他的文件。
        通常在系统初始化后才创建设备文件。它要么发生在加载设备驱动程序(系统尚未支持该设备)所在的模块时,要么发生在一个热插拔的设备(如USB外围设备)加入系统中时。udev工具集可以自动地创建相应的设备文件,因为设备驱动程序模型支持设备的热插拔。当发现一个新的设备时,内核会产生一个新的进程来执行用户态shell脚本文件/sbin/hotplug,并将新设备上的有用信息作为环境变量传递给shell脚本。用户态脚本文件读取配置文件信息并关注完成新设备初始化所必须的任何操作。如果安装了udev工具集,脚本文件也会在/dev目录下创建适当的设备文件。
3.2、设备文件的VFS处理
        虽然设备文件也在系统的目录树中,但是它们和普通文件以及目录文件有根本的不同。当进程访问普通文件时,它会通过文件系统访问磁盘分区中的一些数据块;而在进程访问设备文件时,它只要驱动硬件设备就可以了。例如,进程可以访问一个设备文件以连接到计算机的温度计读取房间的温度。为应用程序隐藏设备文件与普通文件之间的差异正是VFS的责任。
        让我们假定进程在设备文件(块或字符类型)上执行open()系统调用。从本质上说,相应的服务例程解析到设备文件的路径名,并建立相应的索引节点对象、目录项对象和文件对象。
四、设备驱动程序
        设备驱动程序是内核例程的集合,它使得硬件设备响应控制设备的编程接口,而该接口是一组规范的VFS函数集(open,read,lseek,ioctl等等)。这些函数的实际实现由设备驱动程序全权负责。由于每个设备都有一个唯一的I/O控制器,因此就有唯一的命令和唯一的状态信息,所以大部分I/O设备都有自己的驱动程序。
4.1、注册设备驱动程序
        我们知道在设备文件上发出的每个系统调用都由内核转化为对相应设备驱动程序的对应函数的调用。为了完成这个操作,设备驱动程序必须注册自己。换句话说,注册一个设备驱动程序意味着分配一个新的device_driver描述符,将其插入到设备驱动程序模型的数据结构中,并把它与对应的设备文件(可能是多个设备文件)连接起来。如果设备文件对应的驱动程序以前没有注册,则对该设备文件的访问会返回错误码-ENODEV。
        如果设备驱动程序被静态地编译进内核,则它的注册在内核初始化阶段进行。相反,如果驱动程序是作为一个内核模块来编译的,则它的注册在模块装入时进行。在后一种情况下,设备驱动程序也可以在模块卸载时注销自己。
        注册设备驱动程序时,内核会寻找可能由该驱动程序处理但还尚未获得支持的硬件设备。为了做到这点,内核主要依靠相关的总线类型描述符bus_type的match方法,以及device_driver对象的probe方法。如果探测到可被驱动程序处理的硬件设备,内核会分配一个设备对象,然后调用device_register()函数把设备插入设备驱动程序模型中。
4.2、初始化设备驱动程序
        对设备驱动程序进行注册和初始化是两件不同的事。设备驱动程序应当尽快被注册,以便用户态应用程序能通过相应的设备文件使用它。相反,设备驱动程序在最后可能时刻才被初始化。事实上,初始化驱动程序意味着分配宝贵的系统资源,这些资源因此就对其他驱动程序不可用了。
        为了确保资源在需要是能够获得,在获得后不再被请求,设备驱动程序通常采用下列模式:
        * 引用计数器记录当前访问设备文件的进程数。在设备文件的open方法中计数器被增加,在release方法中被减少
        * open方法在增加引用计数器的值之前先检查它。如果计数器为0,则设备驱动程序必须分配资源并激活硬件设备上的中断和DMA
        * release方法在减少使用计数器的值之后检查它。如果计数器为0,说明已经没有进程使用这个硬件设备。如果是这样,该方法将禁止I/O控制器上的中断和DMA,然后释放所分配的资源
4.3、监控I/O操作
        在任何情况下,启动I/O操作的设备驱动程序都必须依靠一种监控技术在I/O操作终止或超时时发出信号。
        在终止操作的情况下,设备驱动程序读取I/O接口状态寄存器的内容来确定I/O操作是否成功执行。在超时的情况下,驱动程序知道一定出了问题,因为完成操作所允许的最大时间间隔已经用完,但什么也没做。
        监控I/O操作结束的两种可用技术分别成为轮询模式(polling mode)和中断模式(interrupt mode)。
4.3.1、轮询模式
        CPU依照这种技术重复检查(轮询)设备的状态寄存器,直到寄存器的值表明I/O操作已经完成为止。第五章自旋锁提到一种基于轮询的技术:当处理器试图获得一个繁忙的自旋锁时,它就重复地查询变量的值,直到该值变为0为止。但是,应用到I/O操作中的轮询技术更加巧妙,这是因为驱动程序还必须记住检查可能的超时。
        如果完成I/O操作需要的时间相对较多,比如说毫秒级,那么这种模式就变得低效,因为CPU花费宝贵的机器周期去等待I/O操作的完成。在这种情况下,在每次轮询操作之后,可以通过把schedule()的调用插入到循环内部来自愿放弃CPU
4.3.2、中断模式
        如果I/O控制器能够通过IRQ线发出I/O操作结束的信号,那么中断模式才能被使用。
        中断处理程序从设备的输入寄存器中读字符,并把它存放在foo全局变量指向的驱动程序描述符foo_dev_t的data字段中。然后设置intr表示,并调用wake_up_interruptible()函数唤醒在foo->wait等待队列上阻塞的进程。
4.4、访问I/O共享存储器

4.5、直接内存访问(DMA)
        现在所有的PC都包含一个辅助的DMA电路,它可以用来控制在RAM和I/O设备之间数据的传送。DMA一旦被激活,就可以自行传送数据;当数据传送完成之后,DMA发出一个中断请求。当CPU和DMA同时访问同一内存单元时,所产生的冲突由一个名为内存仲裁器的硬件电路来解决。
4.5.1、同步DMA和异步DMA
        设备驱动程序可以采用两种方式使用DMA,分别是同步DMA和异步DMA。第一种方式,数据的传送是由进程触发的;而第二种方式,数据的传送是由硬件设备触发的。
4.5.2、DMA传送的辅助函数

4.5.3、总线地址
        一般而言,启动一次数据传送前,设备驱动程序必须确保DMA电路可以直接访问RAM内存单元。
        内核为什么应该关心总线地址呢?这是因为在DMA操作中,数据传送不需要CPU的参与;I/O设备和DMA电路直接驱动数据总线。因此,当核开始DMA操作时,必须把所涉及的内存缓冲区总线地址或写入DMA适当的I/O端口,或写入I/O设备适当的I/O端口。
4.5.4、高速缓存的一致性
        开发人员在下面两种DMA映射类型中进行选择:
        一致性DMA映射:使用这种映射方式时,内核必须保证内存与硬件设备间高速缓存一致性不是什么问题,也就是说CPU在RAM内存单元上所执行的每个写操作对硬件设备而言都是立即可见的,反过来也一样。这种映射方式也称为“同步的”或“一致的”。
        流式DMA映射:使用这种映射方式时,设备驱动程序必须了解高速缓存一致性问题,这可以使用适当的同步辅助函数来解决。这种映射方式也称为“异步的”或“非一致性的”。
        一般来说,如果CPU和DMA处理器以不可预知的方式去访问一个缓冲区,那么必须强制使用一致性DMA映射方式。其他情形下,流式DMA映射方式更可取,因为在一些体系结构中处理一致性DMA映射是很麻烦的,并且可能导致更低的系统性能。
4.5.5、一致性DMA映射的辅助函数
        dma_alloc_coherent()
4.5.6、流式DMA映射的辅助函数

4.7、内核支持的级别

五、字符设备驱动程序
        字符设备驱动程序是由一个cdev结构描述的。
        cdev_alloc()函数的功能是动态地分配cdev描述符,并初始化内嵌的kobject数据结构,因此在引用计数器的值变为0时会自动释放该描述符。
        cdev_add()函数的功能是在设备驱动程序模型中注册一个cdev描述符。它初始化cdev描述符中的dev和count字段,然后调用kobj_map()函数。kobj_map()则依次建立设备驱动程序模型的数据结构,把设备号范围复制到设备驱动程序的描述符中。
5.1、分配设备号
        为了记录目前已经分配了哪些字符设备号,内核使用散列表chrdevs,表的大小不超过设备号范围。两个不同的设备号范围可能共享同一个主设备号,但是范围不能重叠,因此它们的次设备号应该完全不同。chrdevs包含255个表项,由于散列函数屏蔽了主设备号的高四位----因此,主设备号的个数少于255个,它们被散列到不同的表项中。每个表项指向冲突链表的第一个元素,而该链表是按主、次设备号的递增顺序进行排序的。
        本质上可以采用两种方法为字符设备驱动程序分配一个范围内的设备号。所有新的设备驱动程序使用第一种方法,该方法使用register_chrdev_region()函数和alloc_chrdev_region()函数为驱动程序分配任意范围内的设备号。
        上述函数并不执行cdev_add(),因此设备驱动程序在所要求的设备号范围被成功分配时必须执行cdev_add()函数。
        第二种方法使用register_chrdev()函数,它分配一个固定的设备号范围,该范围包含唯一一个主设备号以及0~255的次设备号。在这种情形下,设备驱动程序不必调用cdev_add()函数。
5.1.1、register_chrdev_region()函数和alloc_chrdev_region()函数

5.1.2、register_chrdev()函数
        驱动程序使用register_chrdev()函数时需要一个老式的设备号范围:一个单独的主设备号和0~255的次设备号范围。该函数接收的参数为:请求的主设备号major(如果是0则动态分配)、设备驱动程序的名称name和一个指针fops(它指向设备号范围内的特定字符设备文件的文件操作表)。
5.2、访问字符设备驱动程序

5.3、字符设备的缓冲策略
        这可以结合两种不同的技术做到:
        * 使用DMA方式传送数据块。
        * 运用两个或多个元素的循环缓冲区,每个元素具有一个数据块的大小。当一个中断(发送一个信号表明新的数据块已被读入)发生时,中断处理程序把指针移到循环缓冲区的下一个元素,以便将来的数据会存放在一个空元素中。相反,只要驱动程序把数据成功拷贝到用户地址空间,就释放循环缓冲区中的元素,以便用它来保存从硬件设备传送来的新数据。
        循环缓冲区的作用是消除CPU负载的峰值;即使接收数据的用户态应用程序因为其他高优先级任务而慢下来,DMA也要能够继续填充循环缓冲区中的元素,因为中断处理程序代表当前运行的进程执行。

你可能感兴趣的:(《深入理解Linux内核》笔记)