如何实现一个文件系统(四)

文件系统的读写

    要自己创建文件系统必须知道文件系统需要那些操作,各种操作的功能范围,所以我们下面的内容就是分析Linux文件系统的文件读写过程,从中获得文件系统的基本功能函数信息和作用范围。

打开文件

在对文件进行写前,必须先打开文件。打开文件的目的是为了使得目标文件能和当前进程关联,同时需要将目标文件的索引节点从磁盘载入内存,并初始化。

open操作主要包含以下几个工作要做(实际多数工作由sys_open()完成):

l  1 分配文件描述符号。

l  2 获得新文件对象。

l 3 获得目标文件的目录项对象和其索引节点对象(主要通过open_namei()函数)——具体而言是通过调用索引节点对象(该索引节点或是安装点或是当前目录)的lookup方法找到目录项对应的索引节点号ino,然后调用iget(sb,ino)从磁盘读入相应索引节点并在内核中建立起相应的索引节点(inode)对象(其实还是通过调用sb->s_op->read_inode()超级块提供的方法),最后还要使用d_add(dentry,inode)函数将目录项对象与inode对象连接起来。

l 4 初始化目标文件对象的域,特别是把f_op域设置成索引节点中i_fop指向文件对象的操作表——以后对文件的所有操作将调用该表中的实际方法。

l   5 如果定义了文件操作的open方法(缺省),就调用它。

到此可以看到打开文件后,文件相关的“上下文”、索引节点、目录对象等都已经生成就绪,下一步就是实际的文件读写操作了。

文件读写

用户空间通过read/write系统调用进入内核执行文件操作,read操作通过sys_read内核函数完成相关读操作,write通过sys_write内核函数完成相关写操作。简而言之,sys_read()  和sys_write( )几乎执行相同的步骤,请看下面:

l 1  调用fget( )从fd获取相应文件对象file,并把引用计数器file->f_count减1。

l 2  检查file->f_mode中的标志是否允许请求访问(读或写操作)。

l 3 调用locks_verify_area( )检查对要访问的文件部分是否有强制锁。

l 4  调用file->f_op->read或file->f_op->write来传送数据。两个函数都返回实际传送的字节数。

l 5  调用fput( )以减少引用计数器file->f_count的值

l 6 返回实际传送的字节数。

搞清楚大体流程了吧?但别得意,现在仅仅看到的是文件读写的皮毛。因为这里的读写方法仅仅是VFS提供的抽象方法,具体文件系统的读写操作可不是表面这么简单,接下来我们试试看能否用比较简洁的方法把从这里开始到数据被写入磁盘的复杂过程描述清楚。

现在我们要进入文件系统最复杂的部分——实际读写操作了。f_op->read/f_op->write两个方法属于实际文件系统的读写方法,但是对于基于磁盘的文件系统(必须有I/O操作),比如EXT2等,所使用的实际读写方法都是利用Linux系统已经提供的通用函数——generic_file_read/generic_file_write来完成的,这些通用函数的作用就是确定正被访问的数据所在物理块的位置,并激活块设备驱动程序开始数据传送,它们针对Unix风格的文件系统都能很好地完成功能 ,所以没必要自己再实现专用函数了。下面来分析这些通用函数。

先说读方法:

第一利用给定的文件偏移量(ppos)和读写字节数(count)计算出数据所在页[8][8]的逻辑号(index)。

第二 然后开始传送数据页。

第三 更新文件指针,记录时间戳等收尾工作。

其中最复杂的是第二部,首先内核会检查数据是否已经驻存在页高速缓存(page=find_get_page(mmaping ,index), 其中mammping为页高速缓存对象,index为逻辑页号),如果在高速缓存中发现所需数据而且数据是有效的(通过检查一些标志位,如,PG_uptodate),那么内核就可以从缓存中快速返回需要的页;否则如果页中的数据是无效的,那么内核将分配一个新页面,然后将其加入到页高速缓存中,随即使用address_space对象的readpage方法(mapping->a_ops->readpage(file,page))激活相应的函数以便完成磁盘到页的I/O数据传送。之后[9][9]还要调用file_read_actor()方法把页中的数据拷贝到用户态缓冲区,最好进行一些收尾等工作,如更新标志等。

 到此为止,我们才要开始涉及和系统I/O层打交道的工作,下面我们就来分析readpage函数具体如何激活磁盘到页的I/O传输。

address_space对象的readpage方法存放的是函数地址,该函数激活从物理磁盘到页高速缓存的I/O数据传送。对于普通文件,该函数指针指向block_read_full_page()函数的封装函数。例如,REISEFS文件系统的readpage方法指向下列函数实现:

    intreiserfs _readpage(struct file*file, struct page *page)

    {

    returnblock_read_full_page(page,reiserfs _get_block);

}

需要封装函数是因为block_read_full_page()函数接受的参数为待填充页的页描述符及有助于block_read_full_page()找到正确块的函数get_block的地址。该函数依赖于具体文件系统,作用是把相对于文件开始位置的块号转换为相对于磁盘分区中块位置的逻辑块号。在这里它指向reiserfs_get_block( )函数的地址。

block_read_full_page()函数的目的是对页所在的缓冲区启动页I/O操作,具体将要完成这几方面工作:

n        调用create_empty_buffers(  )为页中包含的所有缓冲区[10][10]分配异步缓冲区首部

n        从页所对应的文件偏移量(page->index域)导出页中第一个块的文件块号

n        初始化缓冲区首部,最主要的工作是通过get_block函数进行磁盘寻址,找到缓冲区的逻辑块号(相对于磁盘分区的开始而不是普通文件的开始)

n         对于页中的每个缓冲区首部,对其调用submit_bh()函数,指定操作类型为READ。

l         接下来的工作就该交给I/O传输层处理了,I/O层负责磁盘访问请求调度和管理传输工作。我们简要分析submit_bt()函数,该函数总体来说目的是向tq_disk任务队列[11][11]提交请求,但它所做的工作颇多,下面就简要分析该函数的行为:

u       从b_blocknr(逻辑块号)和b_size(块大小)两个域确定磁盘上第一个块的扇区号,即b_rsector域的值

u       调用generic_make_request()函数向低级别的驱动程序[12][12]发送请求,它接受的参数为缓冲区首部bh和操作类型rw。而该函数从低级驱动程描述符blk_dev[maj]中获得设备驱动程序请求队列的描述符,接着调用请求队列描述符的make_request_fn方法

   make_request_fn方法是请求队列定义的合并相临请求、排序请求的主要执行函数。它将首先创建请求(实际上就是缓冲头和磁盘扇区的映射关系);然后检查请求队列是否为空:

l         如果请求队列为空,则把新的请求描述符插入其中,而且还要将请求队列描述符插入tq_disk任务队列,随后再调度低级驱动程序策略例程的活动。

l         如果请求队列不为空,则把新的请求描述符插入其中,试图把它与其他已经排队的请求进行组合(使用电梯调度算法)。

低级驱动程序的活动策略函数是request_fn方法。

策略例程通常在新请求插入到空列队后被启动。随后队列中的所有请求要依次进行处理,直到队列为空才结束。

策略例程request_fn(定义在请求结构中)的执行过程如下:

u       策略例程处理队列中的第一个请求并设置块设备控制器,使数据传送完成后产生一个中断。然后策略例程就终止。

u       数据传送完毕后块设备控制器产生中断,中断处理程序就激活下半部分。这个下半部分的处理程序把这个请求从队列中删除(end_request( ))并重新执行策略例程来处理队列中的下一个请求。

好了,读操作说完了,是不是觉得不知所云呀,其实上面仅仅是抽取读操作的骨架简要讲解,具体操作还要复杂得多,下面我们将上面的流程总结一下。

    粗略地分,读操作依次需要经过:

l         用户界面层——负责从用户函数经过系统调用进入内核;

l         基本文件系统层——负责调用文件读方法,从高速缓存中搜索数据页,返回给用户。

l         I/O调度层——负责对请求排队,从而提高吞吐量。

l         I/O传输层——利用任务队列异步操作设备控制器完成数据传输。

请看下图4给出的逻辑流程。

 

图4 读操作流 

写操作和读操作大体相同,不同之处主要在于写页面高速缓存时,稍微麻烦一些,因为写操作不象读操作那样必须和用户空间同步[13][13]执行,所以用户写操作更新了数据内容后往往先存储在页高速缓存中,然后等待页回写后台例程bdflush和kupdate[1][14]等来完成写入磁盘的工作。当然写入请求处理还是要通过上面提到的submit_bh函数[1][15]进行I/O处理的。下面简要介绍写过程:

page=__grab_cache_page(mapping,index,&cached_page,&lru_pvec);

status=a_ops->prepare_write(file,page,offset,offset+bytes);

page_fault=filemap_copy_from_user(page,offset,buf,bytes);

status= a_ops->commit_write(file,page,offset,offset+bytes);

首先,在页高速缓存中搜索需要的页,如果需要的页不在高速缓存中,那么内核在高速缓存中新分配一空闲项;下一步,prepare_write()方法被调用,为页分配异步缓冲区首部;接着数据被从用户空间拷贝到了内核缓冲;最后通过commit_write()函数将对应的基础缓冲区标记为脏,以便随后它们被页回写例程写回到磁盘。

好累呀,到此总算把文件读写过程顺了一遍,大家明白了上述概念后,我们进入最后一部分:Romfs事例分析。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值