linux0.12-9-1-总体功能

[395页]

9-1 总体功能

对硬盘和软盘块设备上数据的读写操作是通过中断程序进行的。内核每次读写的数据量以一个逻辑块(1024字节)为单位,而块设备控制器则是以扇区(512字节)为单位。在处理过程中,使用了读写请求项等待队列来顺序缓冲一次读写多个逻辑块的操作。

(a)当程序需要读取硬盘上的一个逻辑块时,就会向缓冲区管理程序提出申请,而程序的进程则进入睡眠等待状态。
(b)缓冲区管理程序首先在缓冲区中寻找以前是否已经读取过这块数据。
©如果缓冲区中已经有了,就直接将对应的缓冲区块头指针返回给程序并唤醒该程序进程。
(d)若缓冲区中还不存在所要求的数据块,则缓冲管理程序就会调用本章中的低级块读写函数ll_rw_block(),向相应的块设备驱动程序发出一个读数据块的操作请求。
(e)该函数会为此创建一个请求结构项,并插入请求队列中。为了提高读写磁盘的效率,减小磁头移动的距离,在插入请求项时使用了电梯移动算法。
(f)此时,若对应块设备的请求项队列为空,则表明此刻该块设备不忙。于是内核就会立刻向该块设备的控制器发发出读数据命令。
(g)当块设备的控制器将数据读入到指定的缓冲块中后,就会发出中断请求信号,
并调用相应的读命令后处理函数,处理继续读扇区操作或者结束本次请求项的过程。
例如对相应块设备进行关闭操作和设置该缓冲块数据已经更新标志,最后唤醒等待该块数据的进程。

9-1-1 块设备请求项和请求队列

根据上面描述,我们知道低级读写函数ll_rw_block()是通过请求项来与各种块设备建立联系并发出读写请求的。对于各种块设备,内核使用了一张块设备表blk_dev[]来进行管理。每种块设备都在块设备表中占有一项。块设备表中每个块设备项的结构为(摘自后面的blk.h):

struct blk_dev_struct {
	void (*request_fn)(void);//请求项操作的函数指针。
	struct request * current_request;//当前请求项指针。
};
extern struct blk_dev_struct blk_dev[NR_BLK_DEV];//块设备表(数组)(NR_BLK_DEV=7)。

第一个字段是一个函数指针,用于操作相应块设备的请求项。例如,对于硬盘驱动程序,它是do_hd_request(),而对于软盘设备,它就是do_floppy_request()。
第二个字段是当前请求项结构指针,用于指明本块设备目前正在处理的请求项,初始化时都被置成NULL。

块设备表将在内核初始化时,在init/main.c程序调用各设备的初始化函数时被设置。为了 便于扩展,Linus把块设备表建成了一个以主设备号为索引的数组。在Linux0.12中,主设备号有7种,其中,主设备号1、2和3分别对应块设备:虚拟盘、软盘和硬盘。在块设备数组中其他各项都被默认地置成NULL。相关操作函数见表9-1。

当内核发出一个块设备读写或其他操作请求时,ll_rw_block()函数即会根据其参数中指明的操作命令和数据缓冲块头中的设备号,利用对应的请求项操作函数do_XX_request()建立一个块设备请求项,并利用电梯算法插入到请求项队列中。请求项队列由请求项数组中的项构成,共有32项,
每个请求项的数据结构如下所示:

struct request {
	int dev;						//使用的设备号(若为-1,表示该项空闲)。
	int cmd;						//命令(READ或WRITE)。
	int errors;						//操作时产生的错误次数。
	unsigned long sector;			//起始扇区。(1块=2扇区)
	unsigned long nr_sectors;		//读、写扇区数。
	char * buffer;					//数据缓冲区。
	struct task_struct * waiting;	//任务等待操作执行完成的地方。
	struct buffer_head * bh;		//缓冲区头指针(include/linux/fs.h,68)。
	struct request * next;			//指向下一请求项。
};
extern struct request request[NR_REQUEST];//请求项数组(NR_REQUEST=32)。

每个块设备的当前请求指针与请求项数组中该设备的请求项链表共同构成了该设备的请求队列。项与项之间利用字段next指针形成链表。因此块设备想和相关的请求队列形成如图9-1所示结构。

请求项采用数组加链表结构的主要原因是为了满足两个目的:
一是利用请求项的数组结构在搜索空闲请求块时可以进行循环操作,搜索访问时间复杂度为常数,因此 程序可以编制得很简洁;
二是为了满足电梯算法插入请求项操作,因此也需要采用链表结构。

图9-1中示出了硬盘设备当前具有4个请求项,软盘设备具有1个请求项,而虚拟盘设备暂时没有读写请求项。
在这里插入图片描述

对于一个当前空闲的块设备,当ll_rw_block()函数为其建立第一个请求项时,会让该设备的当前请求项指针current_request直接指向刚建立的请求项,并且立刻调用对应设备的请求项操作函数开始执行块设备读写操作。当一个块设备已经有几个请求项组成的链表存在,ll_rw_block()就会 利用电梯算法,根据磁头移动距离最小原则,把新建的请求项插入到链表合适的位置。

另外,为满足读操作的优先权,在为建立新的请求项而搜索请求项数组时,把建立写操作时的空闲项搜索范围限制在整个请求项数组的前2/3范围内,而剩下的1/3请求项专门给读操作建立请求项使用。

9-1-2 块设备访问调度处理

相对于内存来说,访问硬盘和软盘等块设备中的数据是比较耗时并且影响系统性能的操作。由于硬盘(或软盘)磁头寻道操作(即把读写磁头从一个磁道移动到另一个指定磁道上)需要花费很长时间,因此我们有必要在向硬盘控制器发送访问操作命令之前对读/写磁盘扇区数据的顺序进行排序,即对请求项链表中各请求项的顺序进行排序,使得所有请求项访问的磁盘扇区数据块都尽量依次顺序进行操作。在Linux0.1X内核中,请求项排序操作使用的是电梯算法。其操作原理类似于电梯的运行轨迹(向一个方向移动),直到该方向上最后一个"请求"停止层为止。然后执行反向移动。对于磁盘来讲就是磁头一直向盘片圆心方向移动,或者反之向盘片边缘移动。

因此,内核并非按照接收到请求项的顺序直接发送给块设备进行处理,而是需要对请求项的顺序进行处理。我们通常把相关的处理程序称为I/O调度程序。Linux0.1x中的I/O调度程序仅对请求项进行了排序处理,而当前流行的Linux内核(例如2.6.x)的I/O调度程序中还包含 对访问相邻磁盘扇区的两个或多个请求项的合并处理。

9-1-3 块设备操作方式

在系统(内核)与硬盘进行I/O操作时,需要考虑三个对象之间的交互作用。它们是系统、控制器和驱动器(例如硬盘或软盘驱动器),如图9-2所示。系统可以直接向控制器发送命令或等待控制器发出中断请求;控制器在接收到命令后就会控制驱动器的操作,读/写数据或者进行其他操作。因此我们可以把这里控制器发出的中断信号看作是这三者之间的同步操作信号,所经历的操作步骤为:

(a)首先系统指明控制器在执行命令结束而引发的中断过程中应该调用的C函数,然后向块设备控制器发送读、写、复位或其他操作命令。
(b)当控制器完成了指定的命令,会发出中断请求信号,引发系统执行块设备的中断处理过程,并在其中调用指定的C函数对读/写或其他命令进行命令结束后的处理工作。
©对于写盘操作,系统需要在发出了写命令后(使用hd_out())等待控制器给予允许向控制器写数据的响应,即需要查询等待控制器状态寄存器的数据请求服务标志DRQ置位。一旦DRQ置位,系统就可以向控制器缓冲区发送一个扇区的数据。
(d)当控制器把数据全部写入驱动器(或发送错误)以后,还会产生中断请求信号,从而在中断处理过程中执行前面预设置的C函数(write_intr())。这个函数会查询是否还有数据要写。如果 有,系统就再把一个扇区的数据传到控制器缓冲区中,然后再次等待控制器把数据写入驱动器后引发的中断,一直这样重复执行。如果此时所有数据都已经写入驱动器,则该C函数就执行本次写盘结束后的处理工作;唤醒等待该请求项有关数据的相关进程、唤醒等待请求项的进程、释放当前请求项并从链表中删除请求项以及释放锁定的相关缓冲区。最后再调用请求项操作函数去执行下一个读/写盘请求项(若还有的话)。
(e)对于读盘操作,系统在向控制器发送出包括需要读的扇区开始位置、扇区数量等信息的命令后,就 等待控制器产生中断信号。当控制器按照读命令的要求,把指定的一扇区数据从驱动器传到了自己的缓冲区之后就会发出中断请求。从而会执行到前面为读盘操作预设置的C函数(read_intr())。该函数首先把控制器缓冲区中一个扇区的数据放到系统的缓冲区中,调整系统缓冲区中当前写入位置,然后递减需读的扇区 数量。若还有数据要读(递减结果值不为0),则继续等待控制器发出下一个中断信号。若此时所有要求的扇区都已经读到系统缓冲区中,就执行与上面写盘操作一样的结束处理工作。

对于虚拟盘设备,由于它的读写操作不涉及与外部设备之间的同步操作,因此没有上述的 中断处理过程。当前请求项对虚拟设备的读写操作完全在do_rd_request()中实现。

需要注意的是:在向硬盘或软盘控制器发送了读/写或其他命令后,发送命令函数并不会等待所发命令的执行过程,而是立刻返回调用它的程序中,并最终返回到调用块设备读写函数ll_rw_block()的其他程序中去等待块设备IO的完成。

例如高速缓冲区管理程序fs/buffer.c中的读块函数bread()(第267行),在调用了ll_rw_block()之后,就调用等待函数wait_on_buffer()让执行当前内核代码的进程立刻进入睡眠状态,直到相关的设备IO结束,在end_request()函数中被唤醒。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值