在1.2内核版本中,在Linux/fs目录下,有一个block_dev.c文件,里面主要包含了block_read、block_write、block_fsync函数。
先说说我遇到的问题,在块读写函数中,内核根据文件指针f_pos找到当前要读或写的文件的块号,然后在一个循环中,根据要读写的字符数进行循环。然后,问题就来了,我们知道,对于一个文件,其所对应的磁盘块在磁盘上并不一定是连续的,内核在建立文件的时候,是将文件分散存储在磁盘上的,然后将具体的磁盘块号保存在文件i节点的一个数组中,其中还用到了二次、三次间接引用的技术。比如一个文件,内核将其分散存储在磁盘块2,4,8中,则将这些磁盘块存储在此文件i节点数组中。但是,问题就来了,在block_write函数中,读写循环的时候,读写完一个块,则要进行下一个块的读写的时候,内核是直接让块号自增的!而且调用的底层磁盘读写函数,也没有传入任何包含文件i节点的信息,也就是说,block_write函数是根据传入文件的文件指针找到要读写的开始文件块号,然后按顺序读写磁盘的,即使文件指针当前指向块2,若要写入三个块,内核也是写入块2,3,4,而不是2,4,8!
对于这个问题,百思不得其解。经过一番的网上冲浪和对内核源码的跟踪和分析,最终终于找到了答案!
原来,block_read, block_write及block_fsync都是通用函数,i被用来代替任何针对某个驱动程序的函数。他们相当于一种面向底层的驱动程序,也相当是对底层设备进行直接编程。他们面对的是块设备文件的读写,注意一下区别,块设备文件不是通常所说的普通文件,Linux的文件抽象更为抽象,将设备也抽象为文件。我的理解就是,硬盘设备整体作为一个文件,CD_ROM光盘也可抽象为一个文件...最终,对实际设备的访问就变成了对设备文件的访问,但普通文件就不一样了,普通文件是(分散)存储在磁盘上的数据。也可能在一个设备上单独分出一块连续的存储空间,作为一个新的块设备文件???
设备文件是独立于具体的文件系统而存在的,或者说,各种设备的设备文件组成一个单独的文件系统。比如,ext文件系统部署在磁盘设备A上,但是磁盘A也被内核抽象为一个设备文件,而且设备文件不会调用ext文件系统的函数,反之亦然。在linux/drivers/block目录下可以看到linux支持的各种块设备及其驱动程序,在hd.c中,是硬盘的驱动程序,其中定义了硬盘的操作函数集:
static struct file_operations hd_fops = {
NULL, /* lseek - default */
block_read, /* read - general block-dev read */
block_write, /* write - general block-dev write */
NULL, /* readdir - bad */
NULL, /* select */
hd_ioctl, /* ioctl */
NULL, /* mmap */
hd_open, /* open */
hd_release, /* release */
block_fsync /* fsync */
};
可以看到,block_write和block_read被引用了;在 floppy.c中的floppy_write函数中嗲用了ret= block_write(inode, filp, buf, count);说明软盘的操作也是基于块设备操作,可能是在软盘上的文件系统采用的是连续存储的方式;在ide.c中也有以下定义:
static struct file_operations ide_fops = {
NULL, /* lseek - default */
block_read, /* read - general block-dev read */
block_write, /* write - general block-dev write */
NULL, /* readdir - bad */
NULL, /* select */
ide_ioctl, /* ioctl */
NULL, /* mmap */
ide_open, /* open */
ide_release, /* release */
block_fsync /* fsync */
#ifdef CONFIG_BLK_DEV_IDECD
,NULL, /* fasync */
ide_check_media_change,/* check_media_change */
NULL /* revalidate */
#endif CONFIG_BLK_DEV_IDECD
};可以看到,block_write和block_read被引用了.......但是,具体的文件系统是不会应用他们的。
为了证实上述说法,我们可以分析内核具体的一个文件系统的实现代码。以Minix文件系统为例,在Linux/fs/minix目录下是minix文件系统的实现源代码。在file.c文件中的minix_file_write函数,就是minix文件系统对文件的写函数,整体流程和block_write函数一致,但是内部的循环--根据要写入的数据块数,以块为单位进行写操作循序。其中调用函数bh = minix_getblk(inode,pos/BLOCK_SIZE,1);在高速缓冲中寻找是否存在相应的文件块缓冲,注意此函数,参数中传入了要读写文件的i节点以及文件指针对应的文件逻辑块号(连续),这说明其函数内部的实现可以依据此i节点找到文件分散在磁盘上的、不连续的块号。再追踪minix_getblk函数。发现其内部调用inode_getblk(inode,block,create);inode_getblk内部有这样一句p = inode->u.minix_i.i_data + nr;正是这句将逻辑上连续的文件块号转换为磁盘实际存储的不连续的块号!inode->u.minix_i.i_data 表示的正是存储文件分散在磁盘文件上的数据块的块号数组,而nr是数组下标,是连续的,就这样,上层函数利用连续的数组下标,让文件块号看似好像是连续的,但底层函数也正是利用nr数组下标,将连续的“块号”转换为实际的、不连续的块号;但是在block_write函数中,调用的是bh = getblk(dev, block, blocksize);此函数只是传入具体设备的设备号和块号,而没有和普通文件相似的可以找到其分散在磁盘上的块的i节点数据结构。再看minix_file_write函数中的底层写函数,是这样调用的ll_rw_block(READ, 1, &bh);说明每次只读入相应的一块数据,而block_write函数中调用bh = breada(dev,block,block+1,block+2,-1);进行块预读,从调用参数可以看出其是预读设备上连续的三块。其实从block_fsync函数就可以看出来,其刷新的是整个指定的设备的缓冲区。
本文还是说明了这样一个问题,即块设备文件和普通文件的区别。块设备文件的操作函数block_write和block_read函数是对块设备的直接驱动操作,是基于(块)设备的,也是架构在硬件设备之上的;而普通的文件的读写函数是基于具体的文件系统的,是架构于具体的文件系统之上的。