1、理解图图9-5
2、 理解电梯调度算法
[426页]
9-4 ll_rw_blk.c程序
9-4-1 功能描述
该程序主要执行低层块设备读/写操作,是本章所有块设备与系统其他部分的接口程序。其他程序通过调用该程序的低级块读写函数ll_rw_block()来读写块设备中的数据。该函数的主要功能是创建块设备读写请求项,并插入到指定块设备请求队列中。实际的读写操作则是由设备的请求项处理函数request_fn()完成。对于硬盘操作,该函数是do_hd_request();对于软盘操作,该函数是do_fd_request();
对于虚拟盘则是do_rd_request()。若ll_rw_block()为一个块设备建立起一个请求项,并通过测试块设备的当前请求项指针为空而确定设备空闲时,就会设置该新建的请求项为当前请求项,并直接调用request_fn()对该请求项进行操作。否则就会使用电梯算法将新建的请求项插入到该设备的请求项链表中等待处理。而当request_fn()结束对一个请求项的处理,就会把该请求项从链表中删除。
由于request_fn()在每个请求项处理结束时,都会通过中断回调C函数(主要是read_intr()和write_intr())再次调用request_fn()自身去处理链表中其余的请求项,因此,只要设备的请求项链表(或者称为队列)中有未处理的请求项存在,都会陆续地被处理,直到设备的请求项链表为空。当请求项链表空时,request_fn()将不再向驱动控制器发送命令,而是立刻退出。因此,对request_fn()函数的循环调用就此结束。
参见图9-5
对于虚拟盘设备,由于它的读写操作不牵涉到上述与外界硬件设备同步操作,因此没有上述的中断处理过程。当前请求项对虚拟设备的读写操作完全在do_rd_request()中实现。
9-4-2 代码注释
/*
* linux/kernel/blk_dev/ll_rw.c
*
* (C) 1991 Linus Torvalds
*/
/*
* This handles all read/write requests to block devices
*/
#include <errno.h>// 错误号头文件。包含系统中各种出错号。
#include <linux/sched.h>// 调度程序头文件,定义了任务结构task_struct、任务0数据等。
#include <linux/kernel.h>// 内核头文件。含有一些内核常用函数的原形定义。
#include <asm/system.h>// 系统头文件。定义了设置或修改描述符/中断门等的嵌入式汇编宏。
#include "blk.h"// 块设备头文件。定义请求数据结构、块设备数据结构和宏等信息。
/*
* 请求结构中含有加载nr个扇区数据到内存中去的所有必须的信息。
*/
// 请求项数组队列。共有NR_REQUEST = 32个请求项。
struct request request[NR_REQUEST];
/*
* 是用于在请求数组没有空闲项时进程的临时等待处。
*/
struct task_struct * wait_for_request = NULL;
/* blk_dev_struct块设备结构是:(参见文件kernel/blk_drv/blk.h,第45行)
* do_request-address // 对应主设备号的请求处理程序指针。
* current-request // 该设备的下一个请求。
*/
// 块设备数组。该数组使用主设备号作为索引。实际内容将在各块设备驱动程序初始化时填入。
// 例如,硬盘驱动程序初始化时(hd.c,343行),第一条语句即用于设置blk_dev[3]的内容。
struct blk_dev_struct blk_dev[NR_BLK_DEV] = {
{ NULL, NULL }, /* no_dev */
{ NULL, NULL }, /* dev mem */
{ NULL, NULL }, /* dev fd */
{ NULL, NULL }, /* dev hd */
{ NULL, NULL }, /* dev ttyx */
{ NULL, NULL }, /* dev tty */
{ NULL, NULL } /* dev lp */
};
/*
* blk_size数组含有所有块设备的大小(块总数):
* blk_size[MAJOR][MINOR]
* 如果 (!blk_size[MAJOR]),则不必检测子设备的块总数。
*/
// 设备数据块总数指针数组。每个指针项指向指定主设备号的总块数数组。该总块数数组每一
// 项对应子设备号确定的一个子设备上所拥有的数据块总数(1块大小 = 1KB)。
int * blk_size[NR_BLK_DEV] = { NULL, NULL, };
// 锁定指定缓冲块。
// 如果指定的缓冲块已经被其他任务锁定,则使自己睡眠(不可中断地等待),直到被执行解
// 锁缓冲块的任务明确地唤醒。
static inline void lock_buffer(struct buffer_head * bh)
{
cli();// 清中断许可。
while (bh->b_lock)// 如果缓冲区已被锁定则睡眠,直到缓冲区解锁。
sleep_on(&bh->b_wait);
bh->b_lock=1;// 立刻锁定该缓冲区。
sti();// 开中断。
}
// 释放(解锁)锁定的缓冲区。
// 该函数与blk.h文件中的同名函数完全一样。
static inline void unlock_buffer(struct buffer_head * bh)
{
if (!bh->b_lock)// 如果该缓冲区没有被锁定,则打印出错信息。
printk("ll_rw_block.c: buffer not locked\n\r");
bh->b_lock = 0;// 清锁定标志。
wake_up(&bh->b_wait);// 唤醒等待该缓冲区的任务。
}
/*
* add-request()向链表中加入一项请求项。它会关闭中断,
* 这样就能安全地处理请求链表了。
*
* 注意,交换请求总是在其他请求之前操作,并且以它们出
* 现的顺序完成。
*/
向链表中加入请求项。
// 参数dev是指定块设备结构指针,该结构中有处理请求项函数指针和当前正在请求项指针;
// req是已设置好内容的请求项结构指针。
// 本函数把已经设置好的请求项req添加到指定设备的请求项链表中。如果该设备的当前请求
// 请求项指针为空,则可以设置req为当前请求项并立刻调用设备请求项处理函数。否则就把
// req请求项插入到该请求项链表中。
static void add_request(struct blk_dev_struct * dev, struct request * req)
{
struct request * tmp;
req->next = NULL;
cli();// 关中断。
if (req->bh)
req->bh->b_dirt = 0;// 清缓冲区“脏”标志。
// 然后查看指定设备是否有当前请求项,即查看设备是否正忙。如果指定设备dev当前请求项
// (current_request)子段为空,则表示目前该设备没有请求项,本次是第1个请求项,也是
// 唯一的一个。因此可将块设备当前请求指针直接指向该请求项,并立刻执行相应设备的请求
// 函数。
if (!(tmp = dev->current_request)) {
dev->current_request = req;
sti();// 开中断。
(dev->request_fn)();// 执行请求函数,对于硬盘是do_hd_request()。
return;
}
// 如果目前该设备已经有当前请求项在处理,则首先利用电梯算法搜索最佳插入位置,然后将
// 当前请求项插入到请求链表中。在搜索过程中,如果判断出欲插入请求项的缓冲块头指针空,
// 即没有缓冲块,那么就需要找一个项,其已经有可用的缓冲块。因此若当前插入位置(tmp
// 之后)处的空闲项缓冲块头指针不空,就选择这个位置。于是退出循环并把请求项插入此处。
// 最后开中断并退出函数。电梯算法的作用是让磁盘磁头的移动距离最小,从而改善(减少)
// 硬盘访问时间。
// 下面for循环中if语句用于把req所指请求项与请求队列(链表)中已有的请求项作比较,
// 找出req插入该队列的正确位置顺序。然后中断循环,并把req插入到该队列正确位置处。
for ( ; tmp->next ; tmp=tmp->next) {
if (!req->bh)
if (tmp->next->bh)
break;
else
continue;
if ((IN_ORDER(tmp,req) ||
!IN_ORDER(tmp,tmp->next)) &&
IN_ORDER(req,tmp->next))
break;
}
req->next=tmp->next;
tmp->next=req;
sti();
}
创建请求项并插入请求队列中。
// 参数major是主设备号;rw是指定命令;bh是存放数据的缓冲区头指针。
static void make_request(int major,int rw, struct buffer_head * bh)
{
struct request * req;
int rw_ahead;
/* WRITEA/READA是一种特殊情况 - 它们并非必要,所以如果缓冲区已经上锁,*/
/* 我们就不用管它,否则的话它只是一个一般的读操作。 */
// 这里'READ'和'WRITE'后面的'A'字符代表英文单词Ahead,表示提前预读/写数据块的意思。
// 该函数首先对命令READA/WRITEA的情况进行一些处理。对于这两个命令,当指定的缓冲区
// 正在使用而已被上锁时,就放弃预读/写请求。否则就作为普通的READ/WRITE命令进行操作。
// 另外,如果参数给出的命令既不是 READ也不是 WRITE,则表示内核程序有错,显示出错信
// 息并停机。注意,在修改命令之前这里已为参数是否是预读/写命令设置了标志rw_ahead。
if (rw_ahead = (rw == READA || rw == WRITEA)) {
if (bh->b_lock)
return;
if (rw == READA)
rw = READ;
else
rw = WRITE;
}
if (rw!=READ && rw!=WRITE)
panic("Bad block dev command, must be R/W/RA/WA");
lock_buffer(bh);
if ((rw == WRITE && !bh->b_dirt) || (rw == READ && bh->b_uptodate)) {
unlock_buffer(bh);
return;
}
repeat: //110行。
/* 我们不能让队列中全都是写请求项:我们需要为读请求保留一些空间:读操作
* 是优先的。请求队列的后三分之一空间仅用于读请求项。
*/
// 好,现在我们必须为本函数生成并添加读/写请求项了。首先我们需要在请求数组中寻找到
// 一个空闲项(槽)来存放新请求项。搜索过程从请求数组末端开始。根据上述要求,对于读
// 命令请求,我们直接从队列末尾开始搜索,而对于写请求就只能从队列2/3处向队列头处搜
// 索空项填入。于是我们开始从后向前搜索,当请求结构request的设备字段dev值 = -1时,
// 表示该项未被占用(空闲)。如果没有一项是空闲的(此时请求项数组指针已经搜索越过头
// 部),则查看此次请求是否是提前读/写(READA或WRITEA),如果是则放弃此次请求操作。
// 否则让本次请求操作先睡眠(以等待请求队列腾出空项),过一会再来搜索请求队列。
if (rw == READ)
req = request+NR_REQUEST;// 对于读请求,将指针指向队列尾部。
else
req = request+((NR_REQUEST*2)/3);// 对于写请求,指针指向队列2/3处。
/* find an empty request */
while (--req >= request)
if (req->dev<0)
break;
/* 如果没有找到空闲项,则让该次新请求操作睡眠:需检查是否提前读/写 */
if (req < request) { // 如果已搜索到头(队列无空项),
if (rw_ahead) {// 则若是提前读/写请求,则退出。
unlock_buffer(bh);
return;
}
sleep_on(&wait_for_request);// 否则就睡眠,过会再查看请求队列。
goto repeat;// 跳转110行。
}
/* 向空闲请求项中填写请求信息,并将其加入队列中 */
// OK,程序执行到这里表示已找到一个空闲请求项。 于是我们在设置好的新请求项后就调用
// add_request()把它添加到请求队列中,立马退出。请求结构请参见blk_drv/blk.h,23行。
// req->sector是读写操作的起始扇区号,req->buffer是请求项存放数据的缓冲区。
req->dev = bh->b_dev; // 设备号。
req->cmd = rw; // 命令(READ/WRITE)。
req->errors=0; // 操作时产生的错误次数。
req->sector = bh->b_blocknr<<1; // 起始扇区。块号转换成扇区号(1块=2扇区)。
req->nr_sectors = 2; // 本请求项需要读写的扇区数。
req->buffer = bh->b_data; // 请求项缓冲区指针指向需读写的数据缓冲区。
req->waiting = NULL; // 任务等待操作执行完成的地方。
req->bh = bh; // 缓冲块头指针。
req->next = NULL; // 指向下一请求项。
add_request(major+blk_dev,req); // 将请求项加入队列中(blk_dev[major],req)。
}
低级页面读写函数(Low Level Read Write Page)。
// 以页面(4K)为单位访问块设备数据,即每次读/写8个扇区。参见下面ll_rw_blk()函数。
void ll_rw_page(int rw, int dev, int page, char * buffer)
{
struct request * req;
unsigned int major = MAJOR(dev);
// 首先对函数参数的合法性进行检测。如果设备主设备号不存在或者该设备的请求操作函数不
// 存在,则显示出错信息,并返回。如果参数给出的命令既不是 READ也不是 WRITE,则表示
// 内核程序有错,显示出错信息并停机。
if (major >= NR_BLK_DEV || !(blk_dev[major].request_fn)) {
printk("Trying to read nonexistent block-device\n\r");
return;
}
if (rw!=READ && rw!=WRITE)
panic("Bad block dev command, must be R/W");
// 在参数检测操作完成后,我们现在需要为本次操作建立请求项。首先我们需要在请求数组中
// 寻找到一个空闲项(槽)来存放新请求项。搜索过程从请求数组末端开始。于是我们开始从
// 后向前搜索,当请求结构request的设备字段dev值 <0 时,表示该项未被占用(空闲)。
// 如果没有一项是空闲的(此时请求项数组指针已经搜索越过头部),则让本次请求操作先睡
// 眠(以等待请求队列腾出空项),过一会再来搜索请求队列。
repeat: //174行
req = request+NR_REQUEST;// 将指针指向队列尾部。
while (--req >= request)
if (req->dev<0)
break;
if (req < request) {
sleep_on(&wait_for_request);// 睡眠,过会再查看请求队列。
goto repeat;// 跳转到174行去重新搜索。
}
/* 向空闲请求项中填写请求信息,并将其加入队列中 */
// OK,程序执行到这里表示已找到一个空闲请求项。 于是我们设置好新请求项,把当前进程
// 置为不可中断睡眠中断后,就去调用add_request()把它添加到请求队列中,然后直接调用
// 调度函数让当前进程睡眠等待页面从交换设备中读入。这里不象make_request()函数那样
// 直接退出函数而调用了schedule(),是因为make_request()函数仅读2个扇区数据。而这
// 里需要对交换设备读/写8个扇区,需要花较长的时间。因此当前进程肯定需要等待而睡眠。
// 因此这里直接就让进程去睡眠了,省得在程序其他地方还要进行这些判断操作。
req->dev = dev; // 设备号。
req->cmd = rw; // 命令(READ/WRITE)。
req->errors = 0; // 读写操作错误计数。
req->sector = page<<3; // 起始读写扇区。
req->nr_sectors = 8; // 读写扇区数。
req->buffer = buffer; // 数据缓冲区。
req->waiting = current; // 当前进程进入该请求等待队列。
req->bh = NULL; // 无缓冲块头指针(不用高速缓冲)。
req->next = NULL; // 下一个请求项指针。
current->state = TASK_UNINTERRUPTIBLE;// 置为不可中断状态。
add_request(major+blk_dev,req); // 将请求项加入队列中。
schedule();
}
低级数据块读写函数(Low Level Read Write Block)。
// 该函数是块设备驱动程序与系统其他部分的接口函数。通常在fs/buffer.c程序中被调用。
// 主要功能是创建块设备读写请求项并插入到指定块设备请求队列中。实际的读写操作则是
// 由设备的request_fn()函数完成。对于硬盘操作,该函数是do_hd_request();对于软盘
// 操作该函数是do_fd_request();对于虚拟盘则是do_rd_request()。 另外,在调用该函
// 数之前,调用者需要首先把读/写块设备的信息保存在缓冲块头结构中,如设备号、块号。
// 参数:rw – READ、READA、WRITE或WRITEA是命令;bh – 数据缓冲块头指针。
void ll_rw_block(int rw, struct buffer_head * bh)
{
unsigned int major;// 主设备号(对于硬盘是3)。
// 如果设备主设备号不存在或者该设备的请求操作函数不存在,则显示出错信息,并返回。
// 否则创建请求项并插入请求队列。
if ((major=MAJOR(bh->b_dev)) >= NR_BLK_DEV ||
!(blk_dev[major].request_fn)) {
printk("Trying to read nonexistent block-device\n\r");
return;
}
make_request(major,rw,bh);
}
块设备初始化函数,由初始化程序main.c调用。
// 初始化请求数组,将所有请求项置为空闲项(dev = -1)。有32项(NR_REQUEST = 32)。
void blk_dev_init(void)
{
int i;
for (i=0 ; i<NR_REQUEST ; i++) {
request[i].dev = -1;
request[i].next = NULL;
}
}