linux0.11在读写块设备的时候,并不直接对块设备进行操作,而是借由低级的ll_rw_block函数通过请求项来与设备进行联系,也即加入了一个中间环节。当然,你也可以直接对块设备进行操作,但是直接操作会存在一些问题:
1. 同一个进程在写的过程中就不能发出读的请求,反之,在读的过程中也不能发出写的请求。
2. 当一个进程正在对硬盘进行写或者读的时候,其他进程就不能发出硬盘操作命令,只能进入睡眠状态,等待硬盘空闲,而不能做其他事情。
所以要解决上面的问题,最好的方法就是定义一个请求队列,将进程发出的各种请求放在队列中,至于什么时候硬盘开始读写就不用管了,进程就可以执行其它的操作。定义这个队列的另一个优点就是可以使用所谓的电梯算法,在频繁进行磁盘操作的时候可以非常有效的减少磁头移动的总距离,增加磁盘寿命。
linux0.11定义的请求项的数据结构如下:
struct request {
int dev; /* -1 if no request */
int cmd; /* READ or WRITE */
int errors;
unsigned long sector;
unsigned long nr_sectors;
char * buffer;
struct task_struct * waiting;
struct buffer_head * bh;
struct request * next;
};
struct request request[NR_REQUEST];
一共定义了32个请求,然后为每个块设备又再定义一个结构,用于指定设备的请求项操作函数do_xx_request。
struct blk_dev_struct {
void (*request_fn)(void);
struct request * current_request;
};
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 */
};
每种设备占用结构体数组中固定的一项,并且与主设备号序号一致,比如对于硬盘设备,其主设备号是0x03。
在硬盘初始化的时候(hd_init())就已经为硬盘指定了请求项的操作函数do_hd_request,所有进行硬盘读写的操作最终都会调用这个函数。当高层执行块设备读写操作的时候会调用低层ll_rw_block函数获取主设备号,然后又调用make_request函数从请求项队列中获取一个空的请求项,并将信息填入该请求项结构体,最后调用add_request函数。在add_request函数中,首先会判断当前请求项是否正忙,也即判断current_request指针是否为空,如果为空,则表示当前硬盘处于空闲中,因此可以立即对硬盘进行操作,如果current_request指针不为空,表示硬盘正忙,则将当前请求项插入到请求项队列中,注意插入之前会对队列中的请求项以电梯算法进行排序。
整个调用流程如下图所示:
由于块设备的I/O响应较慢,因此在实际的读写过程中需要安排一个缓冲环节,其中高速缓冲区的存在就已经非常好的平衡了文件系统层与设备层之间速度不匹配的情况,而这里在设备层中安排的请求项队列其实也算是一个缓冲环节,采用一个队列数据结构将各种块设备的请求以一定的顺序依次进行响应,这样又再一次提升了操作系统的性能。
———————————————————————————————————–
从上图可以看出普通硬盘主要包括3个部分:磁头,磁道(也称柱面)和扇区。因此一个硬盘的容量就与这3方面有关。
磁头(heads):一张盘片包含两个磁头(正反两面)。
磁道(柱面,cylinders):盘面上一圈一圈的同心圆。
扇区(sectors):磁道上的一小块区域。
注意:尽管越往圆心处磁道周长越小,但是每个磁道上的扇区数是
相等
的。因为越往圆心,每个扇区所占面积将越小。
硬盘读写的最小单位是扇区,想象将硬盘所有磁道展开,然后首尾相连,所有扇区顺序排列,所以要获取或设置硬盘中一个扇区的数据(定位一个扇区),必须向硬盘驱动器提供这3个参数:磁头号,柱面号以及扇区号。比如硬盘的MBR扇区定位为:0磁头,0柱面,1扇区。
硬盘的扇区被上层抽象成逻辑块(block),一个逻辑块占用两个扇区,硬盘开始表示第1块,这样上层程序就可以通过逻辑块号访问硬盘。所以上层要访问硬盘中的某一逻辑块的内容,首先要将该逻辑号转换为
顺序扇区号(sector=block x 2)
,然后将顺序扇区号转换为磁头号,柱面号以及扇区号,最后在该扇区处读写两个扇区的内容。
顺序扇区号转换为磁头号,柱面号以及扇区号的公式如下:
sector / track_secs = 整数是tracks,余数是sec
tracks / dev_heads = 整数是cyl,余数是head
其中:
输入:
sector:顺序扇区号
;dev_heads:磁头总数;track_secs:每磁道的扇区数;tracks:当前磁道总数。
输出:head, cys, sec
参考文献:
赵炯,linux内核完全注释.