linux0.11内核完全剖析 - ll_rw_blk.c

本文解析了Linux内核中的块设备低层读写机制,介绍了核心函数ll_rw_block的工作原理,包括请求项的创建、电梯算法的应用及请求队列管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

声明:

参考《linux内核完全剖析基于linux0.11》--赵炯    节选

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() 函数的
循环调用就此结束。参见下图所示。

            

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


2.代码注释

linux/kernel/blk_drv/ll_rw_blk.c 程序
/*
* linux/kernel/blk_dev/ll_rw.c
*
* (C) 1991 Linus Torvalds
*/

/*
* This handles all read/write requests to block devices
*/
/*
* 该程序处理块设备的所有读/写操作。
*/
#include <errno.h> // 错误号头文件。包含系统中各种出错号。(Linus 从 minix 中引进的)
#include <linux/sched.h> // 调度程序头文件,定义了任务结构 task_struct、初始任务 0 的数据,
// 还有一些有关描述符参数设置和获取的嵌入式汇编函数宏语句。
#include <linux/kernel.h> // 内核头文件。含有一些内核常用函数的原形定义。
#include <asm/system.h> // 系统头文件。定义了设置或修改描述符/中断门等的嵌入式汇编宏。

#include "blk.h" // 块设备头文件。定义请求数据结构、块设备数据结构和宏函数等信息。

/*
* The request-struct contains all necessary data
* to load a nr of sectors into memory
*/
/*
* 请求结构中含有加载 nr 扇区数据到内存中的所有必须的信息。
*/
struct request request[NR_REQUEST];

/*
* used to wait on when there are no free requests
*/
/* 是用于在请求数组没有空闲项时进程的临时等待处 */
struct task_struct * wait_for_request = NULL;

/* blk_dev_struct is:
* do_request-address
* next-request
*/
/* blk_dev_struct 块设备结构是:(kernel/blk_drv/blk.h,23)
* do_request-address // 对应主设备号的请求处理程序指针。
* current-request // 该设备的下一个请求。
*/
// 该数组使用主设备号作为索引。实际内容将在各种块设备驱动程序初始化时填入。例如,硬盘
// 驱动程序进行初始化时(hd.c,343 行),第一条语句即用于设置 blk_dev[3]的内容。
struct blk_dev_struct blk_dev[NR_BLK_DEV] = {
	{ NULL, NULL }, 	/* no_dev */ 	// 0 - 无设备。
	{ NULL, NULL }, 	/* dev mem */ 	// 1 - 内存。
	{ NULL, NULL }, 	/* dev fd */ 	// 2 - 软驱设备。
	{ NULL, NULL }, 	/* dev hd */ 	// 3 - 硬盘设备。
	{ NULL, NULL }, 	/* dev ttyx */ 	// 4 - ttyx 设备。
	{ NULL, NULL }, 	/* dev tty */ 	// 5 - tty 设备。
	{ NULL, NULL } 	/* dev lp */ 	// 6 - lp 打印机设备。
};

// 锁定指定的缓冲区 bh。如果指定的缓冲区已经被其它任务锁定,则使自己睡眠(不可中断地等待),
// 直到被执行解锁缓冲区的任务明确地唤醒。
static inline void lock_buffer(struct buffer_head * bh)
{
	cli(); // 清中断许可。
	while (bh->b_lock) // 如果缓冲区已被锁定,则睡眠,直到缓冲区解锁。
		sleep_on(&bh->b_wait);
	bh->b_lock=1; // 立刻锁定该缓冲区。
	sti(); // 开中断。
}

// 释放(解锁)锁定的缓冲区。
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 adds a request to the linked list.
* It disables interrupts so that it can muck with the
* request-lists in peace.
*/
/*
* add-request()向链表中加入一项请求。它会关闭中断,
* 这样就能安全地处理请求链表了 
*/
//// 向链表中加入请求项。参数 dev 指定块设备,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;
	}
	// 如果目前该设备已经有请求项在等待,则首先利用电梯算法搜索最佳插入位置,然后将当前请求插入
	// 到请求链表中。电梯算法的作用是让磁盘磁头的移动距离最小,从而改善硬盘访问时间。
	for ( ; tmp->next ; tmp=tmp->next)
		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 is special case - it is not really needed, so if the */
	/* buffer is locked, we just forget about it, else it's a normal read */
	/* WRITEA/READA 是一种特殊情况 - 它们并并非必要,所以如果缓冲区已经上锁,*/
	/* 我们就不管它而退出,否则的话就执行一般的读/写操作。 */
	// 这里'READ'和'WRITE'后面的'A'字符代表英文单词 Ahead,表示提前预读/写数据块的意思。
	// 对于命令是 READA/WRITEA 的情况,当指定的缓冲区正在使用,已被上锁时,就放弃预读/写请求。
	// 否则就作为普通的 READ/WRITE 命令进行操作。
	if (rw_ahead = (rw == READA || rw == WRITEA)) {
		if (bh->b_lock)
			return;
		if (rw == READA)
			rw = READ;
		else
			rw = WRITE;
	}
	// 如果命令不是 READ 或 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:
	/* we don't allow the write-requests to fill up the queue completely:
	* we want some room for reads: they take precedence. The last third
	* of the requests are only for reads.
	*/
	/* 我们不能让队列中全都是写请求项:我们需要为读请求保留一些空间:读操作
	* 是优先的。请求队列的后三分之一空间是为读准备的。
	*/
	// 请求项是从请求数组末尾开始搜索空项填入的。根据上述要求,对于读命令请求,可以直接
	// 从队列末尾开始操作,而写请求则只能从队列 2/3 处向队列头处搜索空项填入。
	if (rw == READ)
		req = request+NR_REQUEST; // 对于读请求,将队列指针指向队列尾部。
	else
		req = request+((NR_REQUEST*2)/3); // 对于写请求,队列指针指向队列 2/3 处。
	/* find an empty request */
	/* 搜索一个空请求项 */
	// 从后向前搜索,当请求结构 request 的 dev 字段值=-1 时,表示该项未被占用。
	while (--req >= request)
		if (req->dev<0)
			break;
	/* if none found, sleep on new requests: check for rw_ahead */
	/* 如果没有找到空闲项,则让该次新请求睡眠:需检查是否提前读/写 */
	// 如果没有一项是空闲的(此时 request 数组指针已经搜索越过头部),则查看此次请求是否是
	// 提前读/写(READA 或 WRITEA),如果是则放弃此次请求。否则让本次请求睡眠(等待请求队列
	// 腾出空项),过一会再来搜索请求队列。
	if (req < request) { // 如果请求队列中没有空项,则
		if (rw_ahead) { // 如果是提前读/写请求,则解锁缓冲区,退出。
			unlock_buffer(bh);
			return;
		}
		sleep_on(&wait_for_request); // 否则让本次请求睡眠,过会再查看请求队列。
		goto repeat;
	}
	/* fill up the request-info, and add it to the queue */
	/* 向空闲请求项中填写请求信息,并将其加入队列中 */
	// 程序执行到这里表示已找到一个空闲请求项。请求结构参见(kernel/blk_drv/blk.h,23)。
	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)。
}

//// 低层读写数据块函数,是块设备与系统其它部分的接口函数。
// 该函数在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 调用(init/main.c,128)。
// 初始化请求数组,将所有请求项置为空闲项(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;
	}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值