page cache之文件预读readahead内核源码详解

提起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~page319256个页面,page64作为预读窗口的第1page,被加上了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是文件读取的核心函数,其中就调用文件预读相关函数,源码删减后如下(主要为了说明预读过程):

  1. static void do_generic_file_read(struct file *filp, loff_t *ppos,//文件指针偏移
  2.         read_descriptor_t *desc, read_actor_t actor)
  3. {
  4.     //文件页高速缓存核心结构
  5.     struct address_space *mapping = filp->f_mapping;
  6.     struct inode *inode = mapping->host;
  7.     struct file_ra_state *ra = &filp->f_ra;//预读窗口结构体
  8.     pgoff_t index;
  9.     pgoff_t last_index;
  10.     pgoff_t prev_index;
  11.     unsigned long offset;      /* offset into pagecache page */
  12.     unsigned int prev_offset;
  13. int error;
  14.     /*ppos文件指针为本次读取的起始地址,计算出要读取起始文件页page的索引index */
  15.     index = *ppos >> PAGE_CACHE_SHIFT;
  16.     prev_index = ra->prev_pos >> PAGE_CACHE_SHIFT;
  17.     prev_offset = ra->prev_pos & (PAGE_CACHE_SIZE-1);
  18.     //计算出本次要读取的文件结束地址的文件页page索引,desc->count是本次读取的文件字节数
  19.     last_index = (*ppos + desc->count + PAGE_CACHE_SIZE-1) >> PAGE_CACHE_SHIFT;
  20.     //文件指针ppos不足4K的余数
  21.     offset = *ppos & ~PAGE_CACHE_MASK;
  22.    
  23.     for (;;) {
  24.         struct page *page;
  25.         pgoff_t end_index;
  26.         loff_t isize;
  27.         unsigned long nr, ret;
  28.        
  29. find_page:
  30.         //要读取index索引的文件页是否有文件缓存页page,准确说这个文件页对应的4K文件数据已经读取到了这个文件页page对应的内存
  31.         page = find_get_page(mapping, index);
  32.         if (!page) {
  33.             //index对应的文件页没有缓存page,开始同步预读
  34.             page_cache_sync_readahead(mapping,
  35.                     ra, filp,
  36.                     index, last_index - index);
  37.         }
  38.         //如果当前page设置了"PG_Readahead"预读标记位,说明本次读取的文件页数据正好命中上一次的预读窗口的文件页
  39.         if (PageReadahead(page)) {
  40.             //这里发起异步文件预读
  41.             page_cache_async_readahead(mapping,
  42.                     ra, filp, page,
  43.                     index, last_index - index);
  44.         }
  45.        
  46.         if (!PageUptodate(page)) {
  47.             //尝试对page加锁,如果page之前已经被其他进程加锁则加锁失败返回0,否则当前进程对page加锁成功并返回1
  48.             if (!trylock_page(page))//尝试lock page,对pagePG_locked标记
  49.                 goto page_not_up_to_date; //加锁失败则goto page_not_up_to_date
  50.                
  51.             unlock_page(page);
  52.         }
  53. page_ok: //page对应的文件数据已经读取到了page指向的内存
  54.    
  55.         nr = PAGE_CACHE_SIZE;
  56.         /*nr-offset=4k-offset,相减结果是本轮读取到有效文件数据量,小于等于4K。如果初次是从文件1K偏移地址读取,非4K对齐,则nr =4K-1K=3K,即第一轮循环读取的有效文件数据量最多只有3K。下边offset += ret offset &= ~PAGE_CACHE_MASK折腾后,offset始终是0。如此从第2轮读文件循环开始,offset始终是0nr始终是4K*/
  57.         nr = nr - offset;
  58.         //prev_index保存最新一次读取的文件页page的索引
  59.         prev_index = index;
  60.        
  61.         //read系统调用传入的用户空间buf赋值nr字节的数据量,本轮循环读取到的有效数据量<=4k,并且desc->count减去nr。返回值是向用户空间buf复制的文件数据量,<=4k
  62.         ret = actor(desc, page, offset, nr);//file_read_actor
  63.         //offset加上本次循环实际读取字节数
  64.         offset += ret;
  65.         //offset超过一个page大小,令index为当前缓存页page的一个page索引,下轮循环读取这个新的page对应的文件数据
  66.         index += offset >> PAGE_CACHE_SHIFT;
  67.         //offset被赋值为在新的内存页page里的偏移
  68.         offset &= ~PAGE_CACHE_MASK;
  69.    
  70.         //prev_offset保存最新一次读取的文件页里的偏移
  71.         prev_offset = offset;
  72.         page_cache_release(page);
  73.        
  74.         /需读取文件数据还没读完,继续循环
  75.         if (ret == nr && desc->count)
  76.             continue;
  77.         //到这里应该是文件数据读取完了,结束本次文件读取
  78.         goto out;
  79.        
  80. page_not_up_to_date://执行到这里,说明需要等待page文件页对应的文件数据被读取page文件页指向的内存
  81.         //等待page文件页对应的文件数据被读取到page文件页指向的内存,然后被唤醒,会获取PG_locked
  82.         error = lock_page_killable(page);
  83.        
  84. page_not_up_to_date_locked:
  85.         //page缓存页对应的文件页数据已经读取到page指向的内存
  86.         if (PageUptodate(page)) {
  87.             unlock_page(page); //PG_locked解锁
  88.             //page缓存页数据读取ok了,跳到page_ok
  89.             goto page_ok;
  90.         }
  91.         .............//删减一大片
  92. out:
  93.         //prev_index保存最新一次读取的文件页page的索引,prev_offset保存最新一次读取的文件页里的偏移
  94.         ra->prev_pos = prev_index;
  95.         ra->prev_pos <<= PAGE_CACHE_SHIFT;
  96.         ra->prev_pos |= prev_offset;
  97.         //最新的文件指针
  98.         *ppos = ((loff_t)index << PAGE_CACHE_SHIFT) + offset;
  99.         file_accessed(filp);
  100.     }
  101. }

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函数源码,源码有删减

  1. /*
  2. hit_readahead_marker:同步预读false,异步预读true
  3. offset:do_generic_file_read函数里,本轮for循环要读取的文件页page索引
  4. req_size:do_generic_file_read函数里的last_index - index,就是本次read系统调用还剩余多少个没读取的文件页page*/
  5. static unsigned long
  6. ondemand_readahead(struct address_space *mapping,
  7.            struct file_ra_state *ra, struct file *filp,
  8.            bool hit_readahead_marker, pgoff_t offset,//
  9.            unsigned long req_size)
  10. {
  11.     //不同系统不一样,虚拟机里测试时max=2048
  12.     unsigned long max = max_sane_readahead(ra->ra_pages);
  13.     if (!offset)//第一次读文件成立,从文件头开始预读
  14.         goto initial_readahead;
  15.        
  16.     ...............
  17.     if (hit_readahead_marker) {//异步预读成立
  18.         pgoff_t start;
  19.         rcu_read_lock();
  20.         //从本次要实际读取的文件页page索引offset后开始搜索:radix tree找到第一个hole indexhole index索引对应的page还没创建,对应索引的文件4K数据还没读取到这个page(简单说这是第一个还没跟文件建立映射的文件页,这个page是个空洞hole)。这个hole index就是本次预读窗口的第一个page索引
  21.         start = radix_tree_next_hole(&mapping->page_tree, offset+1,max);
  22.         rcu_read_unlock();
  23.         if (!start || start - offset > max)
  24.             return 0;
  25.        
  26.         ra->start = start;//该文件第一个hole index,是预读窗口的第一个page
  27.         //预读窗口大小(预读page文件页数),这是啥算法,就用个预读窗口起始page索引-本次要读取的文件页索引糊弄?
  28.         ra->size = start - offset;  /* old async_size */
  29.         ra->size += req_size;//预读窗口大小再加上本次read系统调用还剩余多少个没读取文件页page
  30.         //根据预读窗口大小和max重新计算预读窗口大小,折腾
  31.         ra->size = get_next_ra_size(ra, max);
  32.         //ra->async_size初值与ra->size一直,异步预读才会这样
  33.         ra->async_size = ra->size;
  34.         goto readit;
  35.     }
  36. ................
  37.     //这里似乎很少执行到
  38.     return __do_page_cache_readahead(mapping, filp, offset, req_size, 0);
  39. initial_readahead://第一次读文件在这里
  40.     //本轮for循环要读取文件页page索引,预读的第一个文件页索引
  41.     ra->start = offset;
  42.     //根据最大预读pagemax调整预读总page数,并赋于ra->size
  43.     ra->size = get_init_ra_size(req_size, max);//req_size4
  44.     /*如果预读总pagera->size大于还剩余没读取的文件页pagereq_size,则ra->async_size被赋值二者差值,否则被赋值ra->size.。什么情况ra->size大于req_size,目前发现发生在同步预读时。比如read读取文件第一执行到do_generic_file_read....->ondemand_readaheadreq_size=16ra->size在这里是64,则ra->async_size=64-16=48。之后将会预读64page,但是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文件页有重叠,异步预读二者没重叠*/
  45.     ra->async_size = ra->size > req_size ? ra->size - req_size : ra->size;
  46. readit:
  47.     //本次要实际读取的文件页page索引offset等于预读窗口的起始page索引?一般情况应该不成立,二般情况可能成立
  48.     if (offset == ra->start && ra->size == ra->async_size) {
  49.         ra->async_size = get_next_ra_size(ra, max);
  50.         ra->size += ra->async_size;
  51.     }
  52.     //这里实际完成page预读,里边调用的也是__do_page_cache_readahead()函数
  53.     return ra_submit(ra, mapping, filp);   
  54. }
  55. unsigned long ra_submit(struct file_ra_state *ra,
  56.                struct address_space *mapping, struct file *filp)
  57. {
  58.     int actual;
  59.     actual = __do_page_cache_readahead(mapping, filp,
  60.                     ra->start, ra->size, ra->async_size);
  61.     return actual;
  62. }

首先说一下预读窗口结构体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函数后边有详细注释,这里不再啰嗦了。

  1. static int   __do_page_cache_readahead(struct address_space *mapping, struct file *filp,
  2.             pgoff_t offset, unsigned long nr_to_read,
  3.             unsigned long lookahead_size)
  4. {
  5.     struct inode *inode = mapping->host;
  6.     struct page *page;
  7.     unsigned long end_index;   
  8.     LIST_HEAD(page_pool);
  9.     int page_idx;
  10.     int ret = 0;
  11.     loff_t isize = i_size_read(inode);
  12.     if (isize == 0)
  13.         goto out;
  14.     end_index = ((isize - 1) >> PAGE_CACHE_SHIFT);
  15.     //nr_to_read是上层传入的预读的总文件页数,offset是预读的起始文件页
  16.     for (page_idx = 0; page_idx < nr_to_read; page_idx++) {
  17.         //page_offset预读的文件页索引
  18.         pgoff_t page_offset = offset + page_idx;
  19.         if (page_offset > end_index)
  20.             break;
  21.         rcu_read_lock();
  22.         //按照page_offset这个page索引,在文件页高速缓存radix tree中查找是否是否已经有了该page
  23.         page = radix_tree_lookup(&mapping->page_tree, page_offset);
  24.         rcu_read_unlock();
  25.         if (page)
  26.             continue;
  27.         //radix tree找不到页索引是page_offsetpage,则分配一个新的page
  28.         page = page_cache_alloc_readahead(mapping);
  29.         if (!page)
  30.             break;
  31.         page->index = page_offset;//新分配的page的页索引
  32.         list_add(&page->lru, &page_pool);//把预读的page添加到page_pool链表
  33.        
  34.         //是本次真正预读的第一个page,预读窗口pagera->size减去真正预读的pagera->async_size的结果
  35.         if (page_idx == nr_to_read - lookahead_size)
  36.             SetPageReadahead(page);//设置该page"PageReadahead"预读标记
  37.         ret++;
  38.     }
  39.     //ret表示预读的page
  40.     if (ret)
  41.         read_pages(mapping, filp, &page_pool, ret);//这里调用文件系统read接口发起读取文件数据到page文件页指向的内存
  42. out:
  43.     return ret;
  44. }

__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文件页的指向的内存。

  1. static int read_pages(struct address_space *mapping, struct file *filp,
  2.         struct list_head *pages, unsigned nr_pages)//预读的pagestruct list_head *pages这个链表,nr_pages是预读page
  3. {
  4.     struct blk_plug plug;
  5.     unsigned page_idx;
  6.     int ret;
  7.     blk_start_plug(&plug);
  8.     if (mapping->a_ops->readpages) {
  9.         //具体文件系统read函数ext4_readpages,实际测试执行的是这个函数,一次读取nr_pagespage
  10.         ret = mapping->a_ops->readpages(filp, mapping, pages, nr_pages);
  11.         /* Clean up the remaining pages */
  12.         put_pages_list(pages);
  13.         goto out;
  14.     }
  15.     ......
  16. out:
  17.     //真正启动文件系统block层磁盘数据传输,异步的,执行完该函数并不能保证文件数据已经传输page文件页指向的内存
  18.     blk_finish_plug(&plug);
  19. }  

 注意,在这个函数流程会执行__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()的传参。

  1. static unsigned long
  2. ondemand_readahead(struct address_space *mapping,
  3.            struct file_ra_state *ra, struct file *filp,
  4.            bool hit_readahead_marker, pgoff_t offset,
  5.            unsigned long req_size)
  6. 1:hit_readahead_marker=0
  7. 2:offset=0读取的文件第一个文件页索引
  8. 3:req_size=16 本次read还剩下16个文件页没读取,就是第1次读取的16个文件页
  9. ra->start=0ra->size=64,ra->async_size== ra->size-req_size=48
  10. static int
  11. __do_page_cache_readahead(struct address_space *mapping, struct file *filp,
  12.             pgoff_t offset, unsigned long nr_to_read,
  13.             unsigned long lookahead_size)
  14. 1:offset=0 ra->start,预读窗口的第一个文件页索引
  15. 2:nr_to_read=64 ra->size,预读窗口设定预读的page数,总读取的page
  16. 3:lookahead_size=48 ra->async_size,实际只有64-16=48page是预读的

第一次读文件的过程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()的传参。

  1. static unsigned long
  2. ondemand_readahead(struct address_space *mapping,
  3.            struct file_ra_state *ra, struct file *filp,
  4.            bool hit_readahead_marker, pgoff_t offset,
  5.            unsigned long req_size)
  6. 1:hit_readahead_marker=1 异步预读
  7. 2:offset=16读取的文件第一个文件页索引
  8. 3:req_size=16 本次read还剩下16个文件页没读取,就是第2次读取的16个文件页
  9. ra->start=64ra->size=256,ra->async_size== ra->size= 256
  10. static int
  11. __do_page_cache_readahead(struct address_space *mapping, struct file *filp,
  12.             pgoff_t offset, unsigned long nr_to_read,
  13.             unsigned long lookahead_size)
  14. 1:offset=64 ra->start,预读窗口预读的第一个文件页索引
  15. 2:nr_to_read=256 ra->size,预读窗口设定预读page数,总读取的page
  16. 3:lookahead_size=256 ra->async_size,本次预读实际读取的page数,与预期预读的pagera->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就是要被标记“预读”。

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值