访问文件模式
访问文件的模式有多种。我们在本章考虑如下几种情况:
规范模式
规范模式下文件打开后,标志O_SYNC与0_DIRECT清0,而且它的内容是由系统调用read()和write()来存取。
系统调用read()将阻塞调用进程,直到数据被拷贝进用户态地址空间(内核允许返回的字节数少于要求的字节数)。
但系统调用write()不同,它在数据被拷贝到页高速缓存(延迟写)后就马上结束。这会在“读写文件”这一节详细阐述。
同步模式
同步模式下文件打开后,标志0_SYNC置1或稍后由系统调用fcntl()对其置1。
这个标志只影响写操作(读操作总是会阻塞),它将阻塞调用进程,直到数据被有效地写入磁盘。这也会在“读写文件”这一节详细阐述。
内存映射模式
内存映射模式下文件打开后,应用程序发出系统调用mmap()将文件映射到内存中。
因此,文件就成为RAM中的一个字节数组,应用程序就可以直接访问数组元素,而不需用系统调用read()、write()或lseek()。这将在“内存映射”这一节详细阐述。
直接I/O模式
直接I/O模式下文件打开后,标志0_DIRECT置1。
任何读写操作都将数据在用户态地址空间与磁盘间直接传送而不通过页高速缓存。这将在“直接I/O传送”这一节详细阐述。
(标志O_SYNC和O_DIRECT的值可以有四种组合。)
异步模式
异步模式下,文件的访问可以有两种方法,
即通过一组POSIX APl或Linux特有的系统调用来实现。所谓异步模式就是数据传输请求并不阻塞调用进程,而是在后台执行,
同时应用程序继续它的正常运行。这将在“异步I/O”这一节详细阐述。
读写文件
在第十二章的“read()和write()系统调用”一节中已经说明了read()和write()系统调用是如何实现的。
相应的服务例程最终会调用文件对象的read和write方法,这两个方法可能依赖文件系统。
对磁盘文件系统来说,这些方法能够确定正被访问的数据所在物理块的位置,并激活块设备驱动程序开始数据传送。
读文件是基于页的,内核总是一次传送几个完整的数据页。如果进程发出read()系统调用来读取一些字节,而这些数据还不在RAM中,
那么,内核就要分配一个新页框,并使用文件的适当部分来填充这个页,把该页加入页高速缓存,最后把所请求的字节拷贝到进程地址空间中。
对于大部分文件系统来说,从文件中读取一个数据页就等同于在磁盘上查找所请求的数据存放在哪些块上。
只要这个过程完成了,内核就可以通过向通用块层提交适当的I/O操作来填充这些页。
事实上,大多数磁盘文件系统的read方法是由名为generic_file_read()的通用函数实现的。
对基于磁盘的文件来说,写操作的处理相当复杂,因为文件大小可以改变,因此内核可能会分配磁盘上的一些物理块。
当然,这个过程到底如何实现要取决于文件系统的类型。
不过,很多磁盘文件系统是通过通用函数generic_file_write()实现它们的write方法的。
这样的文件系统如Ext2、System V/Coherent/Xenix及Minix。
另一方面,还有几个文件系统(如日志文件系统和网络文件系统)通过自定义的函数实现它们的write方法。
从文件中读取数据
让我们讨论一下generic_file_read()函数,该函数实现了几乎所有磁盘文件系统中的普通文件及任何块设备文件的read方法。该函数作用于以下参数:
filp
文件对象的地址
buf
用户态线性区的线性地址,从文件中读出的数据必须存放在这里
count
要读取的字符个数
ppos
指向一个变量的指针,该变量存放读操作开始处的文件偏移量(通常为filp文件对象的f_pos字段)
第一步,函数初始化两个描述符。
第一个描述符存放在类型为iovec的局部变量local_iov 中;
它包含用户态缓冲区的地址(buf)与长度(count),该缓冲区用来存放待读文件中的数据。
第二个描述符存放在类型为kiocb的局部变量kiocb中;它用来跟踪正在运行的同步和异步I/O操作的完成状态。
kiocb描述符的主要字段描述如表16-1所示。
函数generic_file_read()通过执行宏init_sync_kiocb来初始化描述符kiocb,并设置一个同步操作对象的有关字段。
具体地说就是,该宏设置ki_key字段为KIOCB_SYNC_KEY、ki_filp字段为filp、ki_obj字段为current。
然后,generic_file_read()调用__generic_file_aio_read()并将刚填完的iovec 和kiocb描述符地址传给它。
后面这个函数返回一个值,这个值通常就是从文件有效读入的字节数。generic_file_read()返回值后结束。
函数_generic_file_aio_read()是所有文件系统实现同步和异步读操作所使用的通用例程。该函数接受四个参数:
kiocb描述符的地址iocb,iovec描述符数组的地址iov、数组的长度和存放文件当前指针的一个变量的地址ppos。
iovec描述符数组被函数generic_file_read()调用时只有一个元素,该元素描述待接收数据的用户态缓冲区(注1)。
我们现在来说明函数__generic_file_aio_read()的操作。为简单起见,我们只针对最常见的情形,即对页高速缓存文件的系统调用read()所引发的同步操作。
本章后面我们会阐述该函数执行的其他情形。同样,我们不讨论如何对错误和异常的处理。
该函数执行的步骤如下:
1. 调用access_ok()来检查iovec描述符所描述的用户态缓冲区是否有效。
因为起始地址和长度已经从sys_read()服务例程得到,因此在使用前需要对它们进行检查(参见第十章“验证参数”一节)。
如果参数无效,则返回错误代码-EFAULT。
2. 建立一个读操作描述符,也就是一个read_descriptor_t类型的数据结构。该结构存放与单个用户态缓冲相关的文件读操作的当前状态。
该描述符的字段参见表16-2。
3. 调用函数do_generic_file_read(),
传送给它文件对象指针filp、文件偏移量指针ppos、刚分配的读操作描述符的地址和函数file_read_actor()的地址(后面还会阐述)。
4. 返回拷贝到用户态缓冲区的字节数,即read_descriptor_t数据结构中written字段的值。
函数do_generic_file_read()从磁盘读入所请求的页并把它们拷贝到用户态缓冲区。具体执行如下步骤:
1. 获得要读取的文件对应的address_space对象;它的地址存放在filp->f_mapping。
2. 获得地址空间对象的所有者,即索引节点对象,它将拥有填充了文件数据的页面。
它的地址存放在address_space对象的host字段中。
如果所读文件是块设备文件,那么所有者就不是由filp->f_dentry->d_inode所指向的索引节点对象,
而是bdev特殊文件系统中的索引节点对象。
3. 把文件看作细分的数据页(每页4096字节),并从文件指针*ppos导出第一个请求字节所在页的逻辑号,即地址空间中的页索引,
并把它存放在index局部变量中。也把第一个请求字节在页内的偏移量存放在offset局部变量中。
4.开始一个循环来读入包含请求字节的所有页,要读数据的字节数存放在read_descriptor_t描述符的count字段中。
在一次单独的循环期间,函数通过执行下列的子步骤来传送一个数据页:
a.如果index*4096+offset超过存放在索引节点对象的i_size字段中的文件大小,则从循环退出,并跳到第5步。
b.调用cond_resched()来检查当前进程的标志TIF_NEED_RESCHED。如果该标志置位,则调用函数schedule()。
c.如果有预读的页,则调用page_cache_readahead()读入这些页面。我们在后面“文件的预读”一节讨论预读。
d.调用find_get_page(),并传入指向address_space对象的指针及索引值作为参数;
它将查找页高速缓存以找到包含所请求数据的页描述符(如果有的话)。
e.如果find_get_page()返回NULL指针,则所请求的页不在页高速缓存中。如果这样,它将执行如下步骤:
(1)调用handle_ra_miss()来调整预读系统的参数。
(2)分配一个新页。
(3)调用add_to_page_cache()插入该新页描述符到页高速缓存中。记住该函数将新页的PG_locked标志置位。
(4)调用lru_cache_add()插入新页描述符到LRU链表(参见第十七章)。
(5)跳到第4j步,开始读文件数据。
f.如果函数已运行至此,说明页已经位于页高速缓存中。检查标志PG_uptodate;
如果置位,则页所存数据是最新的,因此无需从磁盘读数据。跳到第4m步。
g.页中的数据是无效的,因此必须从磁盘读取。函数通过调用lock-page()函数获取对页的互斥访问。
正如第十五章“页高速缓存的处理函数”一节中所描述的,如果PG_locked已经置位,则lock_page()阻塞当前进程直到标志被清0。
h.现在页已由当前进程锁定。然而,另一个进程也许会在上一步之前已从页高速缓存中删除该页,那么,它就要检查页描述符的mapping字段是否为NULL。
在这种情形下,它将调用unlock_page()来解锁页,减少它的引用计数(find get_page()增加计数),并跳回第4a步来重读同一页。
i.如果函数已运行至此,说明页已被锁定且在页高速缓存中。再次检查标志PG_uptodate,
因为另一个内核控制路径可能已经完成第4f步和第4g步的必要读操作。如果标志置位,则调用unlock_page()并跳至第4m来跳过读操作。
j.现在真正的I/O操作可以开始了,调用文件的address_space对象之readpage方法。
相应的函数会负责激活磁盘到页之间的I/O数据传输。我们以后再讨论该函数对普通文件与块设备文件都会做些什么。
k.如果标志PG_uptodate还没有置位,则它会等待直到调用lock_page()函数后页被有效读入。
该页在第4g步中锁定,一旦读操作完成就被解锁。因此当前进程在I/O数据传输完成时才停止睡眠。
l.如果index超出文件包含的页数(该数是通过将inode对象的i_size字段的值除于4096得到的),那么它将减少页的引用计数器,并跳出循环至第5步。
这种情况发生在这个正被本进程读的文件同时有其他进程正在删减它的时候。
m.将应被拷入用户态缓冲区的页中的字节数存放在局部变量nr中。
这个值应该等于页的大小(4096字节),除非offset非0(这只发生在读请求书的首尾页时)或请求数据不全在该文件中。
n. 调用mark_page_accessed()将标志PG_referenced或PG_active置位,从而表示该页正被访问并且不应该被换出(参见第十七章)。
如果同一文件(或它的一部分)在do_generic_file_read()的后续执行中要读几次,那么这个步骤只在第一次读时执行。
o.现在到了把页中的数据拷贝到用户态缓冲区的时候了。为了这么做,o_generic_file_read()调用file_read_actor()函数,
该函数的地址作为参数传递。file_read_actor()执行下列步骤:
(1)调用kmap(),该函数为处于高端内存中的页建立永久的内核映射(参见第八章“高端内存页框的内核映射“一节)。
(2)调用__copy_to_user(),该函数把页中的数据拷贝到用户态地址空间(参见第十章“访问进程地址空间“一节)。
注意,这个操作在访问用户态地址空间时如果有缺页异常将会阻塞进程。
(3)调用kunmap()来释放页的任一永久内核映射。
(4)更新read_descriptor_t描述符的count、written和buf字段。
p.根据传入用户态缓冲区的有效字节数来更新局部变量index和count。一般情况下,
如果页的最后一个字节已拷贝到用户态缓冲区,那么index的值加1而offset的值清0;否则,
index的值不变而offset的值被设为已拷贝到用户态缓冲区的字节数。
q.减少页描述符的引用计数器。
r.如果read_descriptor_t描述符的count字段不为0,那么文件中还有其他数据要读,跳至第4a步继续循环来读文件中的下一页数据。
4. 所有请求的或者说可以读到的数据已读完。函数更新预读数据结构filp->f_ra来标记数据已被顺序从文件读入(参见下一节“文件的预读”)。
5. 把index*4096+offset值赋给*ppos,从而保存以后调用read()和write()进行顺序访问的位置。
6. 调用update_atime()把当前时间存放在文件的索引节点对象的i_atime字段中,并把它标记为脏后返回。
普通文件的readpage方法
我们从前一节看到,do_generic_file_read()反复使用readpage方法把一个个页从磁盘读到内存中。
address_space对象的readpage方法存放的是函数地址,这种函数有效地激活从物理磁盘到页高速缓存的I/O数据传送。
对于普通文件,这个字段通常指向调用mpage_readpage()函数的封装函数。例如,Ext3文件系统的readpage方法由下列函数实现:
int ext3_readpage(struct file *file,struct page *page)
{
return mpage_readpage(page,ext3_get_block);
}
需要封装函数是因为mpage_readpage()函数接收的参数为待填充页的页描述符page及有助于mpage_readpage()找到正确块的函数的地址get_block。
封装函数依赖文件系统并因此能提供适当的函数来得到块。
这个函数把相对于文件开始位置的块号转换为相对于磁盘分区中块位置的逻辑块号(例子参见第十八章)。
当然,后一个参数依赖于普通文件所在文件系统的类型;
在前面的例子中,这个参数就是ext3_get_block()函数的地址。
所传递的get_block函数总是用缓冲区首部来存放有关重要信息,如块设备(b_dev字段)、设备上请求数据的位置(b_blocknr字段)和块状态(b_state字段)。
函数mpage_readpage()在从磁盘读入一页时可选择两种不同的策略。
如果包含请求数据的块在磁盘上是连续的,那么函数就用单个bio描述符向通用块层发出读I/O操作。
而如果不连续,函数就对页上的每一块用不同的bio描述符来读。
get_block函数依赖于文件系统,它的一个重要作用就是:确定文件中的下一块在磁盘上是否也是下一块。
具体地说,mpage_readpage()函数执行下列步骤:
1. 检查页描述符的PG_private字段:
如果置位,则该页是缓冲区页,
也就是该页与描述组成该页的块的缓冲区首部链表相关(参见第十五章“把块存放在页高速缓存中”一节)。
这意味着该页过去已从磁盘读入过,而且页中的块在磁盘上不是相邻的。跳到第11步,用一次读一块的方式读该页。
2. 得到块的大小(存放在page->mapping->host->i_blkbits索引节点字段),
然后计算出访问该页的所有块所需要的两个值,即页中的块数及页中第一块的文件块号,也就是相对于文件起始位置页中第一块的索引。
3. 对于页中的每一块,调用依赖于文件系统的get_block函数,作为参数传递过去,得到逻辑块号,
即相对于磁盘或分区开始位置的块索引。页中所有块的逻辑块号存放在一个本地数组中。
4. 在执行上一步的同时,检查可能发生的异常条件。
具体有这几种情况:当一些块在磁盘上不相邻时,或某块落入“文件洞”内(参见第十八章的“文件的洞”一节)时,
或一个块缓冲区已经由get_block函数写入时。那么跳到第11步,用一次读一块的方式读该页。
5. 如果函数运行至此,说明页中的所有块在磁盘上是相邻的。
然而,它可能是文件中的最后一页,因此页中的一些块可能在磁盘上没有映像。
如果这样的话,它将页中相应的块缓冲区填上0;如果不是这样,它将页描述符的标志PG_mappedtodisk置位。
6. 调用bio_alloc()分配包含单一段的一个新bio描述符,并且分别用块设备描述符地址和页中第一个块的逻辑块号来初始化bi_bdev字段和bi_sector字段。
这两个信息已在上面的第3步中得到。
7. 用页的起始地址、所读数据的首字节偏移量(0)和所读的字节总数设置bio段的bio_vec描述符。
8. 将mpage_end_io_read()函数的地址赋给bio->bi_end_io字段(见下面)。
9. 调用submit_bio(),它将用数据传输的方向设定bi_rw标志,更新每CPU变量page_states来跟踪所读扇区数,
并在bio描述符上调用generic_make_request()函数(参见第十四章的“向I/O调度程序发出请求”一节)。
10.返向0(成功)。
11. 如果函数跳至这里,则页中含有的块在磁盘上不连续。如果页是最新的(PG_uptodate置位),函数就调用unlock_page()来对该页解锁;
否则调用block_read_full_page()用一次读一块的方式读该页(见下面)。
12.返回0(成功)。
函数mpage_end_io_read()是bio的完成方法,一旦I/O数据传输结束它就开始执行。
假定没有I/O错误,该函数将页描述符的标志PC_uptodate置位,
调用unlock_page()来对该页解锁并唤醒任何因为该事件而睡眠的进程,然后调用bio_put()来清除bio描述符。
块设备文件的readpage方法
在第十三章“设备文件的VFS处理”一节和第十四章的“打开块设备文件”一节中,我们讨论了内核如何处理请求以打开块设备文件。
我们还看到init_special_inode()函数如何建立设备的索引节点及blkdev_open()如何完成其打开阶段。
在bdev特殊文件系统中,块设备使用address_space对象,该对象存放在对应块设备索引节点的i_data字段。
不像普通文件(在address_space对象中它的readpage方法依赖于文件所属的文件系统的类型),块设备文件的readpage方法总是相同的。
它是由blkdev_readpage()函数实现的,该函数调用block_read_full_page():
int blkdev_readpage(struct file *file,struct * page page){
return block_read_full_page(page,blkdev_get_block);
}
正如你看到的,这个函数又是一个封装函数,这里是block_read_full_page()函数的封装函数。
这一次,第二个参数也指向一个函数,该函数把相对于文件开始处的文件块号转换为相对于块设备开始处的逻辑块号。
不过,对于块设备文件来说,这两个数是一致的;因此,blkdev_get_block()函数执行下列步骤:
1. 检查页中第一个块的块号是否超过块设备的最后一块的索引值
(存放在bdev->bd_inode->i_size中的块设备大小除以存放在bdev->bd_block_size中的块大小得到该索引值;bdev指向块设备描述符)。
如果超过,那么对于写操作它返回-EIO,而对于读操作它返回0。
(超出块设备读也是不允许的,但不返回错误代码。内核可以对块设备的最后数据试着发出读请求,而得到的缓冲区页只被部分映射)。
2. 设置缓冲区首部的b_dev字段为b_dev。
3. 设置缓冲区首部的b_blocknr字段为文件块号,它将被作为参数传给本函数。
4. 把缓冲区首部的BH_Mapped标志置位,以表明缓冲区首部的b_dev和b_blocknr字段是有效的。
函数block_read_full_page()以一次读一块的方式读一页数据。
正如我们已看到的,当读块设备文件和磁盘上块不相邻的普通文件时都使用该函数。它执行如下步骤:
5. 检查页描述符的标志PG_private,如果置位,则该页与描述组成该页的块的缓冲区首部链表相关(参见第十五章的“把块存放在页高速缓存中”一节);
否则,调用create_empty_buffers()来为该页所含所有块缓冲区分配缓冲区首部。
页中第一个缓冲区的缓冲区首部地址存放在page->private字段中。每个缓冲区首部的b_this_page字段指向该页中下一个缓冲区的缓冲区首部。
6. 从相对于页的文件偏移量(page->index字段)计算出页中第一块的文件块号。
7. 对该页中每个缓冲区的缓冲区首部,执行如下子步骤:
a. 如果标志BH_Uptodate置位,则跳过该缓冲区继续处理该页的下一个缓冲区。
b.如果标志BH_Mapped未置位,并且该块未超出文件尾,则调用依赖于文件系统的get_block函数,
该函数的地址已被作为参数得到。对于普通文件,该函数在文件系统的磁盘数据结构中查找,
得到相对于磁盘或分区开始处的缓冲区逻辑块号。
对于块设备文件,不同的是该函数把文件块号当作逻辑块号。
对这两种情形,函数都将逻辑块号存放在相应缓冲区首部的b_blocknr字段中,并将标志BH_Mapped置位(注2)。
c.再检查标志BH_Uptodate,因为依赖于文件系统的get_block函数可能已触发块1/0操作而更新了缓冲区。
如果BH_Uptodate置位,则继续处理该页的下一个缓冲区。
d.将缓冲区首部的地址存放在局部数组arr中,继续该页的下一个缓冲区。
8. 假如上一步中没遇到“文件洞”,则将该页的标志PG_mappedtodisk置位。
9. 现在局部变量arr中存放了一些缓冲区首部的地址,与其对应的缓冲区的内容不是最新的。
如果数组为空,那么页中的所有缓冲区都是有效的,因此,该函数设置页描述符的PG_uptodate标志,调用unlock_page()对该页解锁并返回。
10. 局部数组arr非空。对数组中的每个缓冲区首部,block_read_full_page()执行下列子步骤:
a.将BH_Lock标志置位。该标志一旦置位,函数将一直等到该缓冲区释放。
b.将缓冲区首部的b_end_io字段设为end_buffer_async_read()函数的地址
(见下面),并将缓冲区首部的BH_Async_Read标志置位。
11. 对局部数组arr中的每个缓冲区首部调用submit_bh(),将操作类型设为READ。
就像我们在前面看到的那样,该函数触发了相应块的I/O数据传输。
12. 返回0。
函数end_buffer_async_read()是缓冲区首部的完成方法。
对块缓冲区的I/O数据传输一结束,它就执行。
假定没有I/O错误,函数将缓冲区首部的BH_Uptodate标志置位而将BH_Async_Read标志清0。
那么,函数就得到包含块缓冲区的缓冲区页描述符(它的地址存放在缓冲区首部的b_page字段中),同时检查是否页中所有块是最新的;
如果是,函数将该页的PG_uptodate标志置位并调用unlock_page()。
文件的预读
很多磁盘的访问都是顺序的。我们在第十八章会看到,普通文件以相邻扇区成组存放在磁盘上,因此很少移动磁头就可以快速检索到文件。
当程序读或拷贝一个文件时,它通常从第一个字节到最后一个字节顺序地访问文件。
因此,在处理进程对同一文件的一系列读请求时,可以从磁盘上很多相邻的扇区读取。
预读(read-ahead)是一种技术,这种技术在于在实际请求前读普通文件或块设备文件的几个相邻的数据页。
在大多数情况下,预读能极大地提高磁盘的性能,因为预读使磁盘控制器处理较少的命令,其中的每条命令都涉及一大组相邻的扇区。
此外,预读还能提高系统的响应能力。顺序读取文件的进程通常不需要等待请求的数据,因为请求的数据已经在RAM中了。
但是,预读对于随机访问的文件是没有用的;在这种情况下,预读实际上是有害的,因为它用无用的信息浪费了页高速缓存的空间。
因此,当内核确定出最近所进行的I/O访问与前一次I/O访问不是顺序的时就减少或停止预读。
文件的预读需要更复杂的算法,这是由于以下几个原因:
· 由于数据是逐页进行读取的,因此预读算法不必考虑页内偏移量,只要考虑所访问的页在文件内部的位置就可以了。
· 只要进程持续地顺序访问一个文件,预读就会逐渐增加。
· 当前的访问与上一次访问不是顺序的时(随机访问),预读就会逐渐减少乃至禁止。
· 当一个进程重复地访问同一页(即只使用文件的很小一部分)时,或者当几乎所有的页都已在页高速缓存内时,预读就必须停止。
低级I/O设备驱动程序必须在合适的时候激活,这样当将来进程需要时,页已传送完毕。
如果请求的第一页紧跟上次访问所请求的最后一页,那么相对于上次的文件访问,内核把文件的这次访问看作是顺序的。
当访问给定文件时,预读算法使用两个页面集,各自对应文件的一个连续区域。这两个页面集分别叫做当前窗(current window)和预读窗(ahead window)。
当前窗内的页是进程请求的页和内核预读的页,且位于页高速缓存内(当前窗内的页不必是最新的,因为I/O数据传输仍可能在运行中)。
当前窗包含进程顺序访问的最后一页,且可能有内核预读但进程未请求的页。
预读窗内的页紧接着当前窗内的页,它们是内核正在预读的页。预读窗内的页都不是进程请求的,但内核假定进程会迟早请求。
当内核认为是顺序访问而且第一页在当前窗内时,它就检查是否建立了预读窗。
如果没有,内核创建一个预读窗并触发相应页的读操作。理想情况下,进程继续从当前窗请求页,同时预读窗的页则正在传送。
当进程请求的页在预读窗,那么预读窗就成为当前窗。
预读算法使用的主要数据结构是file_ra_state描述符,它的字段见表16-3。每个文件对象在它的f_ra字段中存放这样的一个描述符。
当一个文件被打开时,在它的file_ra_state描述符中,除了prev_page和ra_pages 这两个字段,其他的所有字段都置为0。
prev_page字段存放着进程在上一次读操作中所请求页的最后一页的索引。它的初值是-1。
ra_pages字段表示当前窗的最大页数,即对该文件允许的最大预读量。
该字段的初始值(缺省值)存放在该文件所在块设备的backing_dev_info描述符中(参见第十四章的“请求队列描述符”一节)。
一个应用可以修改一个打开文件的ra_pages字段从而调整预读算法;
具体的实现方法是调用posix_fadvise()系统调用,
并传给它命令POSIX_FADV_NORMAL(设最大预读量为缺省值,通常是32页)、
POSIX_FADV_SEQUENTIAL (设最大预读量为缺省值的两倍)和POSIX_FADV_RANDOM(最大预读量为0,从而永久禁止预读)。
flags字段内有两个重要的字段RA_FLAG_MISS和RA_FLAG_INCACHE。
如果已被预读的页不在页高速缓存内(可能的原因是内核为了释放内存而加以收回了,参见第十七章),则第一个标志置位,
这时候下一个要创建的预读窗大小将被缩小。
当内核确定进程请求的最后256页都在页高速缓存内时(连续高速缓存命中数存放在ra->cache_hit字段中),第二个标志置位,
这时内核认为所有的页都已在页高速缓存内,进而关闭预读。
何时执行预读算法?这有下列几种情形:
1.当内核用用户态请求来读文件数据的页时。
这一事件触发page_cache_readahead()函数的调用(参见本章前面“从文件中读取数据”一节有关do_generic_file_read()描述的第4c步)。
2.当内核为文件内存映射分配一页时(参见本章后面“内存映射的请求调页”一节中的filemap_nopage()函数,
它再次调用page_cache_readahead()函数)。
3.当用户态应用执行readahead()系统调用时,它会对某个文件描述符显式触发某预读活动。
4.当用户态应用使用POSIX_FADV_NOREUSE或POSIX_FADV_WILLNEED命令执行posix_fadvise()系统调用时,它会通知内核,
某个范围的文件页不久将要被访问。
5.当用户态应用使用MADV_WILLNEED命令执行madvise()系统调用时,它会通知内核,某个文件内存映射区域中的给定范围的文件页不久将要被访问。
page_cache_readahead()函数
page_cache_readahead()函数处理没有被特殊系统调用显式触发的所有预读操作。
它填写当前窗和预读窗,根据预读命中数更新当前窗和预读窗的大小,也就是根据过去对文件访问预读策略的成功程度来调整。
当内核必须满足对某个文件一页或多页的读请求时,函数就被调用,该函数有下面五个参数:
mapping
描述页所有者的address_space对象指针
ra
包含该页的文件file_ra_state描述符指针
filp
文件对象地址
of fset
文件内页的偏移量
req_size
要完成当前读操作还需要读的页数(注3)