13.4.18 UNIX中设备I/O的实现
前面已经学习过,在UNIX中,设备被当作文件,也就是说,当人们想要向设备写信息的时候,所要做的全部工作就是提交一个写文件的系统调用,除了这里的文件是一个对应/dev目录中设备的特殊设备文件之外,其他没有什么不同。
读者也许会问:内核是如何将写这个特殊设备文件的系统调用在内部转换成写到这个特殊的设备,例如磁盘、打印机或终端等呢?
人们已经知道,设备主要分成两类:(1)字符设备,如终端和打印机;(2)块设备,如磁盘和磁带。字符设备每次接收/发送数据是一个字符。块设备每次接收/发送数据是一个块,这就可能会产生数据缓冲。因此,UNIX将这两类设备当作是各自独立的实体,显然,系统需要不同类型的设备驱动程序驱动这两类设备。系统针对设备的特殊文件维护了一个指明设备是字符设备还是块设备的指示符。
设备文件中有个指示符:指明是字符设备还是块设备,根据指示符选择合适的表:cdevsw或bdevsw。设备文件的索引节点中文件大小字段维护主号与次号,根据次号选择cdevsw或bdevsw中特定的行,根据系统调用指明的操作选择特定例行程序,次号表明具体的设备。
在字符设备或块设备中,UNIX对应每一类设备都有一个设备驱动程序。例如,对所有的显示终端和所有的打印机分别有一个特殊的设备驱动程序。设备类型在UNIX中被称作主号(major number)。在UNIX中,特定设备类型中的每个设备号被称作次号(minor number)。如果想要向终端写字符,所要做的全部工作就是执行对应设备的设备驱动程序(使用主号),并将次号(也就是特定的终端id)作为参数传递。这是基于对同一类型的两个终端而言,设备驱动程序相同的这个假设。设备驱动程序知道只有实际的设备才能执行特定的操作,而实际的设备是按照次号的形式提供给设备驱动程序的。显然,由此可以知道,如果系统连接两类终端,它就需要在字符设备中有两个主号,因为这两类终端被当作需要使用不同设备驱动程序的两种不同类型的设备。
对于属于某类设备的每个设备而言,可以执行大量如打开、读、写和关闭这样的操作。每个操作对应一个例行程序。所有这些例行程序汇总在一起就构成了设备驱动程序。内核根据要执行的操作,将控制传递给相应的例行程序。然而,对磁盘这样的块设备而言,有一个位于读写程序上面的名为"策略"的例行程序。这个策略例行程序也是设备驱动程序的一部分。这个例行程序帮助调度磁臂,优化磁臂的移动。例如,在完成特定的读/写操作后,策略例行程序查找要调度的下一个操作(这取决于磁臂当前位置和磁臂调度算法)。
如果设备可以运行在块模式和字符模式下,那么在表中有针对这两种设备类型的条目。UNIX区分这两种基本类型的设备,因为块设备的设备驱动程序通常会为每个设备分配一定数量的缓冲区。它会用到先取策略,此时读取的块比要用的块多。当请求块时,为了避免额外的I/O系统开销,内核会先在缓冲区中搜索这个块。这是不必要的,同样对字符设备也不可能这样。这些设备的驱动程序要在被称作是"字符链表(clist)"的对象中存储每个字符,clist就是它们的缓冲机构。
不管文件中的差别,将它们全部当作设备的特殊文件。这样做有一个好处:它为应用程序员和系统程序员提供了一个统一而熟悉的接口。另一个好处就是可以使用前面讨论过的保护/权限(rwx)机制控制访问不同的设备。
内核和设备之间的接口被分成两部分:(a)内核到设备驱动程序的接口;(b)设备驱动程序到实际设备(硬件)的接口。内核到设备驱动程序的接口很简单。内核根据涉及的设备类型和操作调用特定的设备驱动程序,检查权限并执行初始缓冲区/字符链表处理工作。完成这个过程之后,第二个接口接管。设备驱动程序开始执行设备硬件使用的指令(例如,I/O可以是内存映射的,编程的I/O或是DMA)。最终,设备驱动程序调用合适的中断处理例行程序在中断发生后进行中断处理。
对应设备的特殊文件和包含索引节点的其他文件很相似。索引节点有一个字段,名为"文件类型",这种情况下,文件类型要么是"字符型",要么是"块型"(对非设备文件而言,它是"DIR"、"FIFO"等)。索引节点还有另一个名为"文件大小"的字段。对这个特殊的文件而言,该字段没有什么意义。因此,它指明文件的主号和次号。内核维护的用于操作设备的数据结构如图13-25所示。
![]() |
(点击查看大图)图13-25 用于设备管理的数据结构 |
图13-25给出了两个主要表格:CDEVSW和BDEVSW,它们分别对应字符设备和块设备。这些表格就是设备管理的核心。这些名称中的第一个字符表示设备类型,也就是说,C表示字符设备,B表示块设备。接下来的三个字符DEV表示设备。最后两个字符SW指明转换表。表格保存要为不同类型设备执行打开、关闭等程序的地址,这些地址由主号指明。
这些表格可以由特殊文件的索引节点得到。内核可以由"类型"字段获取特殊字符或块表和主号,该表格不保存文件大小,它会给内核提供期望使用表中所需的行。例如,对终端而言,图13-25指明在/dev目录中有一个名为/dev/tty1的设备文件。该文件的索引节点类型为"字符型",这样内核进入CDEVSW表中。同样的,索引节点还指明这些终端设备的主号等于0。这样内核进入CDEVSW表中第0行。这之后,要执行的操作(例如,打开、读取)将内核带到特定的一列。这个行列交叉点提供对应该设备类型的设备驱动程序地址。然后,通过以次号形式为参数传递给设备驱动程序的特定设备号,内核就可以针对该类型设备调用这个设备驱动程序。
该图还指明维护表中字符设备情况下字符链表的地址,以及块设备情况下的缓冲区。对每类设备而言,不同的字符链表/缓冲区由内核分配和维护。设备驱动程序负责中断服务程序(ISR),以及可以在该表中找到的相应地址。
下面通过例子加以说明。假定一个程序向终端写信息,那么发生以下情况:
(1) 编译好的代码包括系统调用(和用于写文件的相同),在这种情况下,该文件是一个对应终端的特殊文件。
(2) 系统遇到该系统调用,内核开始处理该系统调用。
(3) 内核访问该设备(或特殊)文件的索引节点,检查进程对该设备的权限。
(4) 内核记录设备类型,也就是特殊文件中的字符或块。它根据该信息从图13-25所示的表中选择合适的表(本例中,第一个对应字符设备的表是CDEVSW)。
(5) 内核由系统调用知道要执行的操作类型(打开、读取等)。在本例中,执行写操作。因此,内核从所选表中选择对应写操作的一栏(也就是CDEVSW)。
(6) 指定设备文件的索引节点,按照文件大小维护主号和次号,内核提取和存储这些信息。
(7) 内核将主号作为所选表的索引使用,选择出特定的行(假定这个数是0)。
(8) 因此,内核选择行号等于0中的设备驱动程序用于字符设备表的写操作。该程序就是CD0WRITE。虽然为了便于理解,在表中用英文方式提供了名称,但实际上内核在该表中维护这些程序的地址(直接的或间接的)。
(9) 现在内核分支到该设备驱动程序,这里它将次号(也就是终端id)作为参数传递给该程序。
(10) 设备驱动程序依次和硬件交互。该交互与硬件无关。它包括实际操作设备和中断矢量的指令。产生设备中断后,系统确认中断设备,并调用合适的中断处理程序。内核同样在表中维护中断服务程序的地址。表还要维护不同设备的字符链表/缓冲区,这些字符链表/缓冲区用于从设备读数据或向设备写数据。内核维护一个指明读/写起始位置的光标。
(11) 此后,按照前面讨论的内容,继续后续处理工作。