提起linux内核的文件预读机制,很多小伙伴肯定是听说过的,为什么预读机制可以提高性能?怎么让初学者快速理解预读机制呢?实践下来觉得还是用示意图举例最简单。本文首先根据实际读取文件的测试数据,用示意图讲解讲解预读机制,然后讲解相关内核源码。源码基于3.10.96详细源码注释见 https://github.com/dongzhiyan-stack/kernel-code-comment。
1 文件预读示意图讲解
测试命令很简单cat test >/dev/null,test文件之前没有读取过,文件数据没在pagecache,文件大小大于100M。实际测试发现cat命令每次read系统调用读取65536字节文件数据。我们知道内核读取文件是以4K文件页为单位,文件页page指向的内存保存文件的4K数据。
比如,文件页page0指向4K内存保存的是test文件0~4K*1地址的数据,文件页page1指向的4K内存保存的是test文件4K*1~4K*2地址的数据………文件页page15指向的4K内存保存的是test文件4K*15~4K*16地址的数据,其他类推。下文若见到page文件页,和page文件页指向的4K内存是一个意思。文件页page与磁盘文件系统中的文件构成映射关系,以4K数据量为单位。如图演示64*4K大小的文件,全部读到文件页page后,该文件与文件页page的映射关系:
实际测试表明,cat test 命令每次read系统调用固定只读取test文件65536字节数据(64K),第一次执行read系统调用读取test文件的过程是:读取该文件地址4K*0~4K*16的数据到page0~ page15文件页,同时触发预读了test文件地址4K*16~4K*64的数据到page16~page63文件页。并且还会执行SetPageReadahead(page)对page16加上Readahead”预读”标记,page16是本次预读窗口的第一个page。预读窗口是什么,简单来说就是一次预读的起始文件页到结束文件页。
来看下第1次test文件读取示意图
标记蓝色是本次实际读取的文件页page,标记棕色是本次预读窗口的页面page,标记红色的是本次预读窗口的第一个page,该page会被加上Readahead“预读”标记。这些颜色标记下文同理。
需要说一下细节,cat读取 test文件地址4K*0~4K*16的数据到page0~ page15文件页的过程,”cat test”进程需要阻塞等待。然后该进程才可以将这16*4K的数据量复制到read系统调用传入的buf,这样才算读取到了本次预期的数据。test文件数据是保存在磁盘文件系统中,而从磁盘向page文件页指向的4K内存读取数据过程是比较耗时的,这个没办法。
但是,触发的同步文件预读,读取 test文件地址4K*16~4K*64的数据到page16~ page63文件页的过程,”cat test”进程并不用阻塞。等”cat test”进程第2次调用vfs_read继续读取test文件地址4K*16后的数据,有概率该文件4K*16~4K*64地址的数据已经读取到了page16~ page63文件页。则”cat test”进程直接从page16~ page63文件页指向的内存复制数据到read系统调用buf即可返回,这就很速度了!不能再等待从磁盘读取数据到page文件页这个缓慢的过程了。这应该就是文件预读提升性能的一个关键点吧。
接着第2次读取test文件示意图
如图所示,第2次读取文件,正常需要读取test文件地址4K*16~4K*32这65536字节的数据到page16~page31这16个page文件页,然后再把这16个page页面的数复制到read系统调用传入的buf即可。但是,第一次读取文件时,已经触发了文件预读:读取 test文件地址4K*16~4K*64的数据到page16~ page63这48个page页面,本文假定第2次读取文件时第1次触发的预读已经完成。所以,第2次读取文件,直接把page16~page31文件页的文件数据复制到read系统调用传入的buf即可返回,有了预读就是快。
但是需要说明一点,请回头看下第1次文件读取时的截图,page16被标记了“预读”。第2次读取page16页面数据前,判断出page16被标记了“预读”,则会再次触发预读,不用问为什么,这是规则。如图所示,page64~page319就是本次预读的256个页面,简单说触发了将test文件地址64*4K~320*4K的数据读取到page64~page319这256页面,并且对page64标记Readahead “预读”,预读窗口的第一个page就是要被标记Readahead “预读”。
第3 次读取test文件
第3次读取test文件地址4K*32~4K*48这65536字节的数据。因为预读的存在,这65536字节的数据已经读取到了page32~page47这16个page页面。直接将page32~page47这16个page页面的文件数据复制到read系统调用传入的buf即可。
第4 次读取test文件
第4次读取test文件地址4K*48~4K*64这65536字节的数据。因为预读的存在,这65536字节的数据已经读取到了page48~page63这16个page页面。直接将page48~page63这16个page页面的文件数据复制到read系统调用传入的buf即可即可。
第5 次读取test文件
第5次读取test文件地址4K*64~4K*80这65536字节的数据。因为预读的存在,这65536字节的数据已经读取到了page64~page79这16个page页面。将page64~page79这16个page页面的文件数据复制到read系统调用传入的buf即可。
注意,本次情况有变,在复制page64这个页面的数据时,page64有Readahead “预读”标记(第2次读取test文件时,触发预读了page64~page319这256个页面,page64作为预读窗口的第1个page,被加上了Readahead “预读”标记),则再次触发第3次预读,预读test文件4K*320~4K*832地址的数据到page320~page831页面。并且,page320作为预读窗口的第一个page,同样也被标记了Readahead “预读”。
我想到小伙伴到这里应该已经大体理解了预读机制:无非是按照一定规则提前把文件一定地址范围的数据预读page页面,这个读取操作是异步的,不用阻塞。然后等下次读取文件时,有较大的概率之前预读的文件数据已经读取到了page文件页指向的内存,则可以直接从这些page文件页指向的内存把本次欲读取的文件数据复制走即可,不用再从磁盘里读写这些文件数据,这效率当然很高。下一节介绍一下相关内核源码。
2 预读机制相关内核源码详解
2.1 预读相关函数讲解
文件read内核源码流程:vfs_read->do_sync_read->generic_file_aio_read->do_generic_file_read,do_generic_file_read是文件读取的核心函数,其中就调用文件预读相关函数,源码删减后如下(主要为了说明预读过程):
- static void do_generic_file_read(struct file *filp, loff_t *ppos,//文件指针偏移
- read_descriptor_t *desc, read_actor_t actor)
- {
- //文件页高速缓存核心结构
- struct address_space *mapping = filp->f_mapping;
- struct inode *inode = mapping->host;
- struct file_ra_state *ra = &filp->f_ra;//预读窗口结构体
- pgoff_t index;
- pgoff_t last_index;
- pgoff_t prev_index;
- unsigned long offset; /* offset into pagecache page */
- unsigned int prev_offset;
- int error;
- /*以ppos文件指针为本次读取的起始地址,计算出要读取起始文件页page的索引index */
- index = *ppos >> PAGE_CACHE_SHIFT;
- prev_index = ra->prev_pos >> PAGE_CACHE_SHIFT;
- prev_offset = ra->prev_pos & (PAGE_CACHE_SIZE-1);
- //计算出本次要读取的文件结束地址的文件页page索引,desc->count是本次读取的文件字节数
- last_index = (*ppos + desc->count + PAGE_CACHE_SIZE-1) >> PAGE_CACHE_SHIFT;
- //文件指针ppos不足4K的余数
- offset = *ppos & ~PAGE_CACHE_MASK;
- for (;;) {
- struct page *page;
- pgoff_t end_index;
- loff_t isize;
- unsigned long nr, ret;
- find_page:
- //要读取index索引的文件页是否有文件缓存页page,准确说这个文件页对应的4K文件数据已经读取到了这个文件页page对应的内存
- page = find_get_page(mapping, index);
- if (!page) {
- //index对应的文件页没有缓存page,开始同步预读
- page_cache_sync_readahead(mapping,
- ra, filp,
- index, last_index - index);
- }
- //如果当前page设置了"PG_Readahead"预读标记位,说明本次读取的文件页数据正好命中上一次的预读窗口的文件页
- if (PageReadahead(page)) {
- //这里发起异步文件预读
- page_cache_async_readahead(mapping,
- ra, filp, page,
- index, last_index - index);
- }
- if (!PageUptodate(page)) {
- //尝试对page加锁,如果page之前已经被其他进程加锁则加锁失败返回0,否则当前进程对page加锁成功并返回1
- if (!trylock_page(page))//尝试lock page,对page上PG_locked标记
- goto page_not_up_to_date; //加锁失败则goto page_not_up_to_date
- unlock_page(page);
- }
- page_ok: //page对应的文件数据已经读取到了page指向的内存
- nr = PAGE_CACHE_SIZE;
- /*即nr-offset=4k-offset,相减结果是本轮读取到有效文件数据量,小于等于4K。如果初次是从文件1K偏移地址读取,非4K对齐,则nr =4K-1K=3K,即第一轮循环读取的有效文件数据量最多只有3K。下边offset += ret 和 offset &= ~PAGE_CACHE_MASK折腾后,offset始终是0。如此从第2轮读文件循环开始,offset始终是0,nr始终是4K*/
- nr = nr - offset;
- //prev_index保存最新一次读取的文件页page的索引
- prev_index = index;
- //向read系统调用传入的用户空间buf赋值nr字节的数据量,本轮循环读取到的有效数据量<=4k,并且desc->count减去nr。返回值是向用户空间buf复制的文件数据量,<=4k。
- ret = actor(desc, page, offset, nr);//file_read_actor,
- //offset加上本次循环实际读取字节数
- offset += ret;
- //offset超过一个page大小,令index为当前缓存页page的一个page索引,下轮循环读取这个新的page对应的文件数据
- index += offset >> PAGE_CACHE_SHIFT;
- //offset被赋值为在新的内存页page里的偏移
- offset &= ~PAGE_CACHE_MASK;
- //prev_offset保存最新一次读取的文件页里的偏移
- prev_offset = offset;
- page_cache_release(page);
- /需读取文件数据还没读完,继续循环
- if (ret == nr && desc->count)
- continue;
- //到这里应该是文件数据读取完了,结束本次文件读取
- goto out;
- page_not_up_to_date://执行到这里,说明需要等待page文件页对应的文件数据被读取page文件页指向的内存
- //等待page文件页对应的文件数据被读取到page文件页指向的内存,然后被唤醒,会获取PG_locked锁
- error = lock_page_killable(page);
- page_not_up_to_date_locked:
- //page缓存页对应的文件页数据已经读取到page指向的内存
- if (PageUptodate(page)) {
- unlock_page(page); //PG_locked解锁
- //page缓存页数据读取ok了,跳到page_ok
- goto page_ok;
- }
- .............//删减一大片
- out:
- //prev_index保存最新一次读取的文件页page的索引,prev_offset保存最新一次读取的文件页里的偏移
- ra->prev_pos = prev_index;
- ra->prev_pos <<= PAGE_CACHE_SHIFT;
- ra->prev_pos |= prev_offset;
- //最新的文件指针
- *ppos = ((loff_t)index << PAGE_CACHE_SHIFT) + offset;
- file_accessed(filp);
- }
- }
do_generic_file_read函数主要是在for循环里,每次循环读取一个文件页(4K),直到读完本次要求读取的的所有数据。首先根据本轮循环读取的文件首数据地址计算出索引index,然后执行page = find_get_page(mapping, index)判断对应索引index的文件页page是否存在。如果不存在,执行page_cache_sync_readahead()发起一次sync预读,预读文件数据到对应文件页page指向的内存。该函数返回后,并不能保证文件数据已经读取到了文件页page指向的内存。
if (!PageUptodate(page))可能不成立,然后goto page_not_up_to_date后执行lock_page_killable(page)休眠。等待对应的文件页数据读取到page指向内存后,会被唤醒,然后goto page_ok,跳到这个分支后,把文件本次读取到的文件数据复制到read系统调用传入的buf,之后循环。
刚才有一点没说,当page有PG_Readahead “预读”标记位, if (PageReadahead(page))成立时,就会执行page_cache_async_readahead()进行一次async预读。同样的,该函数返回后,并不能保证文件数据已经读取到了文件页page指向的内存。之后的流程跟page_cache_sync_readahead()执行后的流程一样。
关于page_cache_sync_readahead()和page_cache_async_readahead()的执行过程,需要详细说明一下page_cache_sync_readahead/page_cache_async_readahead函数发起预读,执行page_cache_sync_readahead/page_cache_async_readahead ->ondemand_readahead->__do_page_cache_readahead->read_pages->ext4_readpages->mpage_readpages->mpage_readpages ->add_to_page_cache_lru->add_to_page_cache->__set_page_locked,发起文件预读,然后对该page加PG_locked锁。接着执行如下流程:
- 1从page_cache_sync_readahead/page_cache_async_readahead返回,因为这个文件预读是异步的,并不能保证已经把磁盘文件数据读取到该文件页page对应的内存,所以接着执行到该if(!PageUptodate(page)),大概率因为还没把文件最新数据读取到page文件页内存,if成立。然后执行if (!trylock_page(page))获取page锁失败,goto page_not_up_to_date。
- 2 goto page_not_up_to_date后,执行lock_page_killable(page)的在page的PG_locked等待队列休眠。
- 3等文件最新数据读取到该page文件页内存,产生中断执行回调函数blk_update_request->bio_endio->mpage_end_io,该函数里SetPageUptodate(page)设置page 的”PageUptodate”状态,还执行unlock_page(page)清理page的PG_locked锁,最后唤醒在page的PG_locked等待队列休眠的进程
- 4 好的,前边在page的PG_locked等待队列休眠的进程被唤醒了,则执行goto page_ok,跳到到这个分支。此时说明文件数据已经读取到了page文件页指向的内存,直接把page文件页数据复制到read系统调用传入的buf即可。
- 5 最后,如果本次要求读取的数据全读完了,read返回。否则继续大的for循环,继续尝试把下一个文件页page数据复制到read系统调用传入的buf。
page_cache_sync_readahead和page_cache_async_readahead函数都是执行ondemand_readahead函数发起的预读,传参不一样。前者是同步预读,后者是异步预读,这里说的同步预读跟正常理解的同步阻塞不是一回事。下边看下ondemand_readahead函数源码,源码有删减
- /*
- hit_readahead_marker:同步预读false,异步预读true
- offset:do_generic_file_read函数里,本轮for循环要读取的文件页page索引
- req_size:do_generic_file_read函数里的last_index - index,就是本次read系统调用还剩余多少个没读取的文件页page数*/
- static unsigned long
- ondemand_readahead(struct address_space *mapping,
- struct file_ra_state *ra, struct file *filp,
- bool hit_readahead_marker, pgoff_t offset,//
- unsigned long req_size)
- {
- //不同系统不一样,虚拟机里测试时max=2048
- unsigned long max = max_sane_readahead(ra->ra_pages);
- if (!offset)//第一次读文件成立,从文件头开始预读
- goto initial_readahead;
- ...............
- if (hit_readahead_marker) {//异步预读成立
- pgoff_t start;
- rcu_read_lock();
- //从本次要实际读取的文件页page索引offset后开始搜索:在radix tree找到第一个hole index,hole index索引对应的page还没创建,对应索引的文件4K数据还没读取到这个page(简单说这是第一个还没跟文件建立映射的文件页,这个page是个空洞hole)。这个hole index就是本次预读窗口的第一个page索引
- start = radix_tree_next_hole(&mapping->page_tree, offset+1,max);
- rcu_read_unlock();
- if (!start || start - offset > max)
- return 0;
- ra->start = start;//该文件第一个hole index,是预读窗口的第一个page
- //预读窗口大小(预读page文件页数),这是啥算法,就用个预读窗口起始page索引-本次要读取的文件页索引糊弄?
- ra->size = start - offset; /* old async_size */
- ra->size += req_size;//预读窗口大小再加上本次read系统调用还剩余多少个没读取文件页page数
- //根据预读窗口大小和max重新计算预读窗口大小,折腾
- ra->size = get_next_ra_size(ra, max);
- //ra->async_size初值与ra->size一直,异步预读才会这样
- ra->async_size = ra->size;
- goto readit;
- }
- ................
- //这里似乎很少执行到
- return __do_page_cache_readahead(mapping, filp, offset, req_size, 0);
- initial_readahead://第一次读文件在这里
- //本轮for循环要读取文件页page索引,预读的第一个文件页索引
- ra->start = offset;
- //根据最大预读page数max调整预读总page数,并赋于ra->size
- ra->size = get_init_ra_size(req_size, max);//req_size的4倍
- /*如果预读总page数ra->size大于还剩余没读取的文件页page数req_size,则ra->async_size被赋值二者差值,否则被赋值ra->size.。什么情况ra->size大于req_size,目前发现发生在同步预读时。比如read读取文件第一执行到do_generic_file_read....->ondemand_readahead,req_size=16,ra->size在这里是64,则ra->async_size=64-16=48。之后将会预读64个page,但是page0~page15是本次read实际要读取的文件页数据,page16~page63是才是本次真正预读的page数。所以我的理解是,ra->async_size表示真正预读的page数,ra->size表示预读窗口的page数。如果是异步预读则ra->async_size=ra->size,如果是同步预读则ra->async_size=ra->size-req_size。同步预读时,预读窗口的文件页page与本次读取文件的page文件页有重叠,异步预读二者没重叠*/
- ra->async_size = ra->size > req_size ? ra->size - req_size : ra->size;
- readit:
- //本次要实际读取的文件页page索引offset等于预读窗口的起始page索引?一般情况应该不成立,二般情况可能成立
- if (offset == ra->start && ra->size == ra->async_size) {
- ra->async_size = get_next_ra_size(ra, max);
- ra->size += ra->async_size;
- }
- //这里实际完成page预读,里边调用的也是__do_page_cache_readahead()函数
- return ra_submit(ra, mapping, filp);
- }
- unsigned long ra_submit(struct file_ra_state *ra,
- struct address_space *mapping, struct file *filp)
- {
- int actual;
- actual = __do_page_cache_readahead(mapping, filp,
- ra->start, ra->size, ra->async_size);
- return actual;
- }
首先说一下预读窗口结构体struct file_ra_state,ondemand_readahead()函数的第2个传参正是struct file_ra_state *ra。它的成员保存了预读窗口的起始文件页page索引(ra->start)和预读窗口大小(ra->size,即预读page数),还有真正预读page数ra->async_size等。
ondemand_readahead()函数主要工作就是通过算法计算出ra->start、ra->size、ra->async_size等,最后执行ra_submit->__do_page_cache_readahead()函数完成预读。关于同步预读、异步预读以及ra->async_size的计算,ondemand_readahead函数后边有详细注释,这里不再啰嗦了。
- static int __do_page_cache_readahead(struct address_space *mapping, struct file *filp,
- pgoff_t offset, unsigned long nr_to_read,
- unsigned long lookahead_size)
- {
- struct inode *inode = mapping->host;
- struct page *page;
- unsigned long end_index;
- LIST_HEAD(page_pool);
- int page_idx;
- int ret = 0;
- loff_t isize = i_size_read(inode);
- if (isize == 0)
- goto out;
- end_index = ((isize - 1) >> PAGE_CACHE_SHIFT);
- //nr_to_read是上层传入的预读的总文件页数,offset是预读的起始文件页
- for (page_idx = 0; page_idx < nr_to_read; page_idx++) {
- //page_offset预读的文件页索引
- pgoff_t page_offset = offset + page_idx;
- if (page_offset > end_index)
- break;
- rcu_read_lock();
- //按照page_offset这个page索引,在文件页高速缓存radix tree中查找是否是否已经有了该page
- page = radix_tree_lookup(&mapping->page_tree, page_offset);
- rcu_read_unlock();
- if (page)
- continue;
- //radix tree找不到页索引是page_offset的page,则分配一个新的page
- page = page_cache_alloc_readahead(mapping);
- if (!page)
- break;
- page->index = page_offset;//新分配的page的页索引
- list_add(&page->lru, &page_pool);//把预读的page添加到page_pool链表
- //是本次真正预读的第一个page,预读窗口page数ra->size减去真正预读的page数ra->async_size的结果
- if (page_idx == nr_to_read - lookahead_size)
- SetPageReadahead(page);//设置该page的"PageReadahead"预读标记
- ret++;
- }
- //ret表示预读的page数
- if (ret)
- read_pages(mapping, filp, &page_pool, ret);//这里调用文件系统read接口发起读取文件数据到page文件页指向的内存
- out:
- return ret;
- }
__do_page_cache_readahead()函数根据预读窗口的起始文件页page索引(ra->start)和预读page数(ra->size),执行radix_tree_lookup()在radix tree中判断对应索引能否找到该page,没有找打则执行page_cache_alloc_readahead()根据该索引分配struct page结构。接着执行SetPageReadahead(page)对第一个真正算是预读的page加上“预读标记”。最后执行read_pages()调用文件系统read接口发起读取文件数据到page文件页的指向的内存。
- static int read_pages(struct address_space *mapping, struct file *filp,
- struct list_head *pages, unsigned nr_pages)//预读的page在struct list_head *pages这个链表,nr_pages是预读page数
- {
- struct blk_plug plug;
- unsigned page_idx;
- int ret;
- blk_start_plug(&plug);
- if (mapping->a_ops->readpages) {
- //具体文件系统read函数ext4_readpages,实际测试执行的是这个函数,一次读取nr_pages个page
- ret = mapping->a_ops->readpages(filp, mapping, pages, nr_pages);
- /* Clean up the remaining pages */
- put_pages_list(pages);
- goto out;
- }
- ......
- out:
- //真正启动文件系统block层磁盘数据传输,异步的,执行完该函数并不能保证文件数据已经传输page文件页指向的内存
- blk_finish_plug(&plug);
- }
注意,在这个函数流程会执行__set_page_locked ()首先占有该page的PG_locked锁,流程是:read_pages->ext4_readpages->mpage_readpages->mpage_readpages ->add_to_page_cache_lru->add_to_page_cache->__set_page_locked
之后,read_pages()->blk_finish_plug()才会真正发起从磁盘文件读取文件数据到文件页page指向的内存。后续如果谁访问该page文件的数据,都需要执行trylock_page(page)等获取page的PG_locked锁失败而休眠。等把文件数据读取到文件页page指向的内存,block层中断回调函数执行SetPageUptodate(page)设置page的"PageUptodate"状态,还执行unlock_page(page)清理page的PG_locked锁,然后唤醒在page的PG_locked等待队列休眠的进程。
2.2 源码与示意图结合讲解
看完2.1节有没有觉得有点蒙,本小节将结合第1节的示意图讲解一下预读过程。
首先是第一次读取test 文件4K*0~4K*16地址的数据,触发了同步预读,函数流程是do_generic_file_read->page_cache_sync_readahead->ondemand_readahead。
在ondemand_readahead函数直接执行goto initial_readahead分支,计算ra->start、ra->size、ra->async_size。接着执行ra_submit->__do_page_cache_readahead,我们看下 ondemand_readahead()和__do_page_cache_readahead()的传参。
- static unsigned long
- ondemand_readahead(struct address_space *mapping,
- struct file_ra_state *ra, struct file *filp,
- bool hit_readahead_marker, pgoff_t offset,
- unsigned long req_size)
- 1:hit_readahead_marker=0
- 2:offset=0读取的文件第一个文件页索引
- 3:req_size=16 本次read还剩下16个文件页没读取,就是第1次读取的16个文件页
- ra->start=0,ra->size=64,ra->async_size== ra->size-req_size=48
- static int
- __do_page_cache_readahead(struct address_space *mapping, struct file *filp,
- pgoff_t offset, unsigned long nr_to_read,
- unsigned long lookahead_size)
- 1:offset=0 即ra->start,预读窗口的第一个文件页索引
- 2:nr_to_read=64 即ra->size,预读窗口设定预读的page数,总读取的page数
- 3:lookahead_size=48 即ra->async_size,实际只有64-16=48个page是预读的
第一次读文件的过程cat test读取 test文件地址4K*0~4K*16的数据到page0~ page15指向的文件页内存过程,”cat test”进程需要阻塞等待。然后该进程才可以将这16*4K的数据量复制到read系统调用传入的buf,才算读取到了本次预期的数据。
本次执行do_generic_file_read->page_cache_sync_readahead->ondemand_readahea触发了预读page0~page63这64个文件页,而page0~page15是本次要求读取的文件页。故实际只预读了page16~page63这48个page,并且对第一个真正预读的page16打上“预读”标记。ra->async_size这个变量有点奇怪,但是现在看来它表示真正预读的page数,剔除了本次read系统调用要求读取的page数。
然后,是第二次读取test 文件16*4k ~32*4k地址的数据,触发了异步预读。函数流程是do_generic_file_read->page_cache_sync_readahead->ondemand_readahead。
ondemand_readahead函数if (hit_readahead_marker)成立,在里边执行radix_tree_next_hole计算第一个文件页hole index,hole index索引的page还没创建,这个hole index是本次预读的第一个文件页索引ra->start,然后计算ra->size、ra->async_size。接着执行ra_submit->__do_page_cache_readahead,我们看下 ondemand_readahead()和__do_page_cache_readahead()的传参。
- static unsigned long
- ondemand_readahead(struct address_space *mapping,
- struct file_ra_state *ra, struct file *filp,
- bool hit_readahead_marker, pgoff_t offset,
- unsigned long req_size)
- 1:hit_readahead_marker=1 异步预读
- 2:offset=16读取的文件第一个文件页索引
- 3:req_size=16 本次read还剩下16个文件页没读取,就是第2次读取的16个文件页
- ra->start=64,ra->size=256,ra->async_size== ra->size= 256
- static int
- __do_page_cache_readahead(struct address_space *mapping, struct file *filp,
- pgoff_t offset, unsigned long nr_to_read,
- unsigned long lookahead_size)
- 1:offset=64 即ra->start,预读窗口预读的第一个文件页索引
- 2:nr_to_read=256 即ra->size,预读窗口设定预读page数,总读取的page数
- 3:lookahead_size=256 即ra->async_size,本次预读实际读取的page数,与预期预读的page数ra->size一致,都是256个。
第2次读取文件,正常需要读取test文件地址4K*16~4K*32这65536字节的数据到page16~page31这16个page页面,然后再把这16个page页面的数据复制到read系统调用传入的buf。但是,第1次读取文件时,已经触发了文件预读:读取 test文件地址4K*16~4K*64的数据到page16~ page63这48个page页面。本文假定第2次读取文件时第1次触发的预读已经完成。所以,第2次读取文件,直接把page16~page31文件页的文件数据复制到read系统调用传入的buf即可返回,这是从内存到内存复制数据。显然第2次读取文件与第1次有很大差异,第2次预读的page没有被第2次实际读取的16个页面覆盖到。
有一点需要注意,第2次读取page16文件页数据前,因为page16被标记了“预读”,则再次触发了异步预读。如图所示,page64~page319就是本次预读的256个页面。简单说触发了将test文件地址64*4K~320*4K的数据读取到page64~page319这256页面,并且对page64标记“预读”标记,预读窗口的第一个page就是要被标记“预读”。