系统的读写

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

打开文件

在对文件进行写前,必须先打开文件。打开文件的目的是为了能使得目标文件能和当前 进程关联,同时需要将目标文件的索引节点从磁盘载入内存,并初始化。 open 操作主要包含以下几个工作要做(实际多数工作由 sys_open()完成):

 1 分配文件描述符号。

 2 获得新文件对象。 7[7] 这里安装的文件系统属于非根文件系统的安装方法。根文件系统安装方法有所区别,请查看相关资料。

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

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

 5 如果定义了文件操作的 open 方法(缺省),就调用它。 到此可以看到打开文件后,文件相关的“上下文”、索引节点、目录对象等都已经生成就绪, 下一步就是实际的文件读写操作了。

文件读写 :

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

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

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

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

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

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

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

搞清楚大体流程了吧?但别得意,现在仅仅看到的是文件读写的皮毛。因为这里的读写 方法仅仅是 VFS 提供的抽象方法,具体文件系统的读写操作可不是表面这么简单,接下来 我们试试看能否用比较简洁的方法把从这里开始到数据被写入磁盘的复杂过程描述清楚。 现在我们要进入文件系统最复杂的部分——实际读写操作了。f_op->read/f_op->write 两 个方法属于实际文件系统的读写方法,但是对于基于磁盘的文件系统(必须有 I/O 操作), 比如 EXT2 等,所使用的实际的读写方法都是利用 Linux 系统业以提供的通用函数 ——generic_file_read/generic_file_write 完成的,这些通用函数的作用是确定正被访问的数据 所在物理块的位置,并激活块设备驱动程序开始数据传送,它们针对 Unix 风格的文件系统 都能很好的完成功能 ,所以没必要自己再实现专用函数了。下面来分析这些通用函数。

先说读方法:

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

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

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

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

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

int reiserfs _readpage(struct file *file, struct page *page)
{
    return block_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 操作,具体将要完成 这几方面工作:

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

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

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

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

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

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

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

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

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

 如果请求队列不为空,则把新的请求描述符插入其中,试图把它与其他已经排队的请求 进行组合(使用电梯调度算法)。 低级驱动程序的活动策略函数是 request_fn 方法。 策略例程通常在新请求插入到空列队后被启动。随后队列中的所有请求要依次进行处 理,直到队列为空才结束。

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

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

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

好了,写操作说完了,是不是觉得不知所云呀,其实上面仅仅是抽取写操作的骨架简要讲解, 具体操作还要复杂得多,下面我们将上面的流程总结一便。 粗略地分,读操作依次需要经过: 用户界面层 ——负责从用户函数经过系统调用进入内核;

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

I/O 调度层 ——负责对请求排队,从而提高吞吐量。 I/O 传输层 ——利用任务列队异步操作设备控制器完成数据传输。

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

写操作和读操作大体相同,不同之处主要在于写页面高速缓存时,稍微麻烦一些,因为写操 作不象读操作那样必须和用户空间同步 13[6] 执行,所以用户写操作更新了数据内容后往往先 存先存储在页高速缓存中,然后等页回写后台例程 bdflush 和 kupdate 14[7] 等来完成写如磁盘 的工作。当然写入请求处理还是要通过上面提到的 submit_bh 函数 15[8] 进行 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 事例分析。