文件预读readahead内核优化提升文件读取性能

之前写过一篇文章《page cache之文件预读readahead内核源码详解》,主要讲解了内核vfs层文件预读内核源码。受内核block层io_uring异步机制的影响,觉得可以对内核预读机制优化,提升文件读取性能。

首先,我们需知晓,内核文件预读机制本身可以一定程度提升文件读取性能,这点在《page cache之文件预读readahead内核源码详解》中就提到过,我们这里再简单提下。比如,我们cat test读取文件,每次read系统调用只读取64K字节的文件数据:

第1次执行read系统调用,读取test文件地址0~64K的文件数据,同时还发起文件预期:读取test文件地址64k~256k的文件数据。注意,这里只是发起文件读取命令,读取test文件的进程要休眠一段时间,等test文件地址0~64K的文件数据从磁盘读取到到文件页page,进程被唤醒,本次的64K数据读取完成。

第2次执行read系统调用读取test文件64k~128k地址的数据,但是大概率这64K大部分的数据已经从磁盘读取到内存了,进程基本不用休眠或者只需休眠很短的时间,就得到这64K文件数据返回。为什么,因为第一次读取test文件0~64K地址的数据时,已经预读了test文件地址64k~256k的文件数据。结果是,第1次读取test文件地址0~64K的64k数据耗时50ms(假设),第2次读取test文件地址64k~128K的64K数据仅耗时10ms,预读提升的性能。但是我觉得,这还不够,预期的性能还可以进一步提升,本文主要基于这个优化思路。

1 现有文件预读机制的缺点分析

先把《page cache之文件预读readahead内核源码详解》介绍预读文件预读的过程介绍下:

执行cat  test命令读取读取文件,test文件大小100M。测试表明,cat test命令执行后,每次read系统调用只读取64K的文件数据。下边5幅图分别是cat test命令执行后,前5次read系统调用读取的文件数据及触发的文件预读数据。

 蓝色框是每次read系统调用读取的64K数据,一共16个page文件页(每个框代表一个page文件页,每个page文件页保存4K文件数据,16*4K=64K)。橙色框是每次read系统调用触发的文件预读,预读的文件数据到page文件页。如图,第1次预读的文件数据量是48*4K,第2次预读的文件数据量是256*4K,第3、4次没有触发文件预读,第5次预读的文件数据量是256*4K。

可以发现,随着read系统调用次数增多,预读的文件数据量越大。并且,并不是每次read系统调用都会触发文件预读。除了read系统调用第一次读取文件,必然会触发同步文件预读(为什么是同步预读,因为此时没有一个预读的page文件页),并且对预读的第一个page加上“预读标记”。之后,每次read系统调用读取文件时,必须当前读取的page文件页有“预读标记”,才会触发异步文件预读(为什么是异步预读,因为此时已经有了N多个预读的page文件页),然后对本次异步预读的第一个page文件页加上“预读标记”。好的,知道了预读过程,提出几个疑问?

1:默认预读的文件数据量太少。就像cat  test读取文件,第1次read系统调用只预读了48*4K文件数据量,假如第2次read系统调用要读取256*4K的数据量,那就要阻塞等待较长时间。

2 为什么预读的文件数据量不多?估计设计者考虑到,触发预读太消耗系统资源吧!预读的函数流程是do_generic_file_read->page_cache_sync_readahead/page_cache_async_readahead->ondemand_readahead->...->__do_page_cache_readahead->read_pages ,这个流程还是比较复杂的。如果预读的文件的数据量非常大,比如预读几个G的文件数据量,则会在这个流程耗时相当长时间。下边我们详细聊聊这点:

把文件读取的核心函数do_generic_file_read()源码简单列下:

  1. //内核文件读取核心函数
  2. static void do_generic_file_read(struct file *filp, loff_t *ppos,
  3.                 read_descriptor_t *desc, read_actor_t actor)
  4. {
  5.     .............
  6.     for (;;) {
  7.         ..........
  8.         //根据本次读取的文件逻辑地址获取映射的page文件页
  9.         page = find_get_page(mapping, index);
  10.         if (!page) {
  11.                 //如果找不到page文件页则触发同步文件预读
  12.                 page_cache_sync_readahead(mapping,
  13.                                 ra, filp,
  14.                                 index, last_index - index);
  15.                 ............
  16.         }
  17.         //如果本次读取page文件页有预读标记位,触发异步文件预读
  18.         if (PageReadahead(page)) {
  19.                 page_cache_async_readahead(mapping,
  20.                                 ra, filp, page,
  21.                                 index, last_index - index);
  22.         }
  23.         //如果page文件页对应的磁盘文件数据还没有读取到page文件页内存
  24.         if (!PageUptodate(page)) {
  25.                 ................
  26.                 //尝试获取page锁失败而休眠,等对应的磁盘文件数据读取到page文件页,再把当前进程唤醒
  27.                 if (!trylock_page(page))
  28.                         goto page_not_up_to_date;
  29.                 ...............
  30.                 unlock_page(page);
  31.         }
  32.     }
  33.     .............
  34. }

第1,该函数里执行page_cache_sync_readahead、page_cache_async_readahead进行同步文件预读、异步文件预读。如果预读的文件数据量很大,这两个函数执行过程将会消耗不少时间。这样反而会拖累进程文件读取效率!

第2,如果预读的文件数据量不多,则do_generic_file_read()里if (!PageUptodate(page)) 有较大概率成立,因为page文件页对应的磁盘文件数据还没有读取到page文件页内存,则接着执行if (!trylock_page(page))而休眠。

第2点举个例子更会清晰,比如第一执行read系统调用预读了文件地址16*4k~16*4k +48*4k这48*4k的文件数据。第二次执行read系统调用读取文件地址16*4k~16*4k +100*4k的文件数据,读取文件地址16*4k~16*4k +48*4k的数据时,有较大的概率这个范围的文件数据已经读取到了page文件页,就不用等待磁盘文件数据读取到对应的page文件页内存。但是读取文件地址16*4k +48*4k~16*4k +100*4k的文件数据时,大概率这个范围的文件数据还没有读取到page文件页内存,读文件的进程只能休眠等待了,这加大了文件读取延时。

总结一下,预读的文件数据量越大,则进程read系统调用后执行到do_generic_file_read()函数,读取本次的文件地址对应的文件数据时,这些文件数据已经读到了对应的page文件页内存,直接copy走即可,基本不用等待从磁盘读取对应的文件数据到page文件页内存,耗时短。但是预读的文件数据量大,预读函数流程do_generic_file_read->page_cache_sync_readahead/page_cache_async_readahead->ondemand_readahead->...->__do_page_cache_readahead->read_pages将会耗时很长,这又加大了do_generic_file_read()函数中的耗时,相互矛盾!为什么不能将预期流程从do_generic_file_read()函数剥离开呢?就是与主读文件进程剥离开,二者尽可能不要相互影响。

于是来了一个idea:可以创建一个内核线程,在内核线程里完成文件预读,预读的数据量可以很大。这样既能保证主文件进程,读取文件的进程read系统调用后执行到do_generic_file_read()函数,读取本次的文件地址对应的文件数据时,这些文件数据已经读到了对应的page文件页内存,直接copy走即可,基本不用等待从磁盘读取对应的文件数据到page文件页内存,耗时短。又避免了do_generic_file_read()里执行预读流程代码耗时太长

2 内核预读的优化思路

把文件预读的内核函数流程再发下do_generic_file_read->page_cache_sync_readahead/page_cache_async_readahead->ondemand_readahead->...->__do_page_cache_readahead,列下源码:

  1. static int
  2. __do_page_cache_readahead(struct address_space *mapping, struct file *filp,
  3.                         pgoff_t offset, unsigned long nr_to_read,
  4.                         unsigned long lookahead_size)
  5. {
  6.     .............
  7.     for (page_idx = 0; page_idx < nr_to_read; page_idx++) {
  8.             pgoff_t page_offset = offset + page_idx;
  9.             if (page_offset > end_index)
  10.                     break;
  11.             rcu_read_lock();
  12.             page = radix_tree_lookup(&mapping->page_tree, page_offset);
  13.             rcu_read_unlock();
  14.             if (page && !radix_tree_exceptional_entry(page))
  15.                     continue;
  16.             page = page_cache_alloc_readahead(mapping);
  17.             if (!page)
  18.                     break;
  19.             page->index = page_offset;
  20.             list_add(&page->lru, &page_pool);
  21.             if (page_idx == nr_to_read - lookahead_size)
  22.                     SetPageReadahead(page);
  23.             ret++;
  24.     }
  25.     if (ret) //执行文件系统read接口真正读取文件数据到page文件页
  26.         read_pages(mapping, filp, &page_pool, ret);
  27.  //page_idx+offset是已经预读的最后一个page文件页索引加1end_index表示文件结束地址对应的page文件页索引。如果page_idx+offset <= end_index成立,说明还有文件数据没有读取,则执行start_async_file_read()继续触发预读(本文介绍的预读优化)
  28.     if(page_idx + offset <= end_index)                                                                                                                            
  29.         //page_idx + offset这个索引的page开始预读
  30.         start_async_file_read(mapping, filp,page_idx + offset);
  31.     ........
  32. }  

在__do_page_cache_readahead()函数里添加了红色那两行代码,主要是执行start_async_file_read()唤醒文件预读内核线程async_file_read这样在读取文件时,第一次触发内核默认预读,执行到__do_page_cache_readahead()函数,就会执行到start_async_file_read()函数。

  1. struct async_file_read_info{
  2.     struct task_struct *task;//保存预读内核线程async_file_read的进程结构
  3.     struct address_space *mapping;//预读的文件struct address_space结构
  4.     struct file *filp;//预读的文件struct file结构
  5.     unsigned long start_read_page;//预读的文件的起始文件页索引
  6.     atomic_t count;//目前控制同时只有一个进程能触发预读内核线程async_file_read
  7. };
  8. struct async_file_read_info async_file_read;
  9. static int start_async_file_read(struct address_space *mapping,struct file *filp,unsigned long start_read_page)
  10. {
  11.     if(strcmp(current->comm,"test") == 0 || strcmp(current->comm,"cat") == 0)
  12.     {
  13.         if(async_file_read.task == NULL){
  14.             async_file_read.task = kthread_create(async_file_read_thread,(void *)&async_file_read,"async_file_read");
  15.             if (IS_ERR(async_file_read.task)) {
  16.                 printk("%s kthread_create fail\n",__func__);
  17.                 return -1;
  18.             }
  19.         }
  20.         //sync_file_read.count0成立并加1,说明这是第一个触发async_file_read的进程,目前支持一个进程async_file_read
  21.         if(atomic_add_return(1,&async_file_read.count) == 1){
  22.             async_file_read.mapping = mapping;
  23.             async_file_read.filp = filp;
  24.             async_file_read.start_read_page = start_read_page;
  25.             //唤醒进程,只支持单个进程async_file_read
  26.             wake_up_process(async_file_read.task);
  27.         }else{
  28.             atomic_dec(&async_file_read.count);
  29.         }
  30.     }
  31.     return 0;
  32. }

首先定义了struct async_file_read_info async_file_read结构,用户保存预读文件相关信息。start_async_file_read()函数主要创建async_file_read内核线程,然后唤醒async_file_read内核线程,在该内核线程函数async_file_read_thread()完成文件预读。函数传参start_read_page是将要预读的起始page文件页索引,赋于async_file_read.start_read_page,async_file_read.mapping保存将要预读的文件 struct address_space结构。

目前限制只有读文件的进程名字是test或者cat时才会唤醒async_file_read内核线程,这点后续会优化,做成类似cgroup可以绑定任何一个进程那样。并且同时只支持一个进程触发async_file_read内核线程进行文件预读,后续会考虑定义一个队列,保存所有欲触发async_file_read内核线程进行文件预读的进程信息,然后从队列一个个取出这些进程,一个个唤醒sync_file_read内核线程进行指定文件的预读。

看下async_file_read内核线程函数async_file_read_thread源码:

  1. static int async_file_read_thread(void *arg)
  2. {
  3.     struct async_file_read_info *async_file_read_tmp = (struct async_file_read_info *)arg;
  4.     while (!kthread_should_stop()) {
  5.         //开始读取文件
  6.         async_file_read_pages(async_file_read_tmp);
  7.        
  8.         if(atomic_read(&async_file_read_tmp->count) > 0)
  9.             atomic_dec(&async_file_read_tmp->count);
  10.         else
  11.             printk("%s %s %d sync_file_read.count:%d\n",__func__,current->comm,current->pid,atomic_read(&async_file_read_tmp->count));
  12.         //设置进程S状态
  13.         set_current_state(TASK_INTERRUPTIBLE);
  14. //休眠
  15.         schedule();
  16.         __set_current_state(TASK_RUNNING);
  17.     }
  18.     async_file_read_tmp->task = NULL;
  19.     return 0;
  20. }

当async_file_read内核线程被唤醒,就会执行它的线程函数async_file_read_thread()。该函数执行async_file_read_pages(async_file_read_tmp)真正预读文件。预读完执行schedule()休眠,循环往复。async_file_read_pages()函数源码如下:

  1. static int async_file_read_pages(struct async_file_read_info * p_async_file_read_info)
  2. {
  3.     struct address_space *mapping = p_async_file_read_info->mapping;
  4.     struct file *filp = p_async_file_read_info->filp;
  5.     pgoff_t page_to_read,start_read_page,page_offset;
  6.     struct inode *inode = mapping->host;
  7.     struct page *page;
  8.     unsigned long end_index;    /* The last page we want to read */
  9.     LIST_HEAD(page_pool);
  10.     int page_idx;
  11.     int ret = 0;
  12.     loff_t isize = i_size_read(inode);
  13.     if (isize == 0)
  14.         return -1;
  15.     //结束文件页索引
  16.     end_index = ((isize - 1) >> PAGE_CACHE_SHIFT);
  17.     //预读的起始page文件页索引
  18.     start_read_page = p_async_file_read_info->start_read_page;
  19.     //预读的page文件页数量,默认一直读到文件结束地址的page文件页
  20.     page_to_read = end_index - p_async_file_read_info->start_read_page;
  21.     for (page_idx = 0; page_idx < page_to_read; page_idx++){
  22.         page_offset = start_read_page + page_idx;
  23.         if (page_offset > end_index)
  24.             break;
  25.         rcu_read_lock();
  26.         //先在radix tree按照page文件页索引查找struct page结构
  27.         page = radix_tree_lookup(&mapping->page_tree, page_offset);
  28.         rcu_read_unlock();
  29.         if (page && !radix_tree_exceptional_entry(page))
  30.             continue;
  31.         //没有找到page结构则根据page文件页索引page_offset分配struct page结构
  32.         page = page_cache_alloc_readahead(mapping);
  33.         if (!page)
  34.             break;
  35.         page->index = page_offset;
  36.         //page结构添加到page_pool链表
  37.         list_add(&page->lru, &page_pool);
  38.         //ret是预读的page文件页数
  39.         ret++;
  40.     }
  41.     if (ret)//执行文件系统read接口真正读取文件数据到page文件页
  42.         read_pages(mapping, filp, &page_pool, ret);
  43.     return 0;
  44. }

可以发现,源码与内核原有的文件预读函数__do_page_cache_readahead()基本一致。就是按照读取的起始文件页索引start_read_page和预读文件页数page_to_read,按照文件页索引分配文件页struct page结构。最后执行read_pages(mapping, filp, &page_pool, ret)把文件数据读取这些page文件页。

这里需要说明一点,默认预读的文件范围是,从起始文件页索引start_read_page一直到结束文件页索引end_index,相当于把当前文件所有数据都读取到page文件页。这种情况在文件很大时,比如几十GB,将会消耗大量内存,这点后续也需要优化,设定每次最大预读文件数据量等。

3内核预读优化的测试

首先编写测试程序test_read.c,省略出错判断代码。

  1. #define FILE_SIZE (1024*1024*1)
  2. int main(int argc,char *argv[ ])
  3. {
  4.     int ret = 0;
  5.     unsigned char *p;
  6.     int fd;
  7.     int i,j;
  8.     struct timeval start,stop;
  9.     int read_count = 0;
  10. p = (unsigned char*)malloc(FILE_SIZE);
  11. …………
  12. //释放pagecache,避免对测试数据有影响
  13.     system("echo 1 >/proc/sys/vm/drop_caches");
  14.     fd = open("test2",O_RDWR|O_SYNC);
  15. …………
  16.     gettimeofday(&start,0);
  17.     while(1)
  18.     {
  19.         ret = read(fd,p,FILE_SIZE);
  20.         if(ret > 0)
  21.             read_count += ret;
  22.          if(ret <= 0)
  23.             goto err;
  24.         //for循环模拟对读取到的数据做一定处理
  25.         for(i = 0;i < 100;i++)
  26.             for(j = 0;j < 1000;j++);
  27.     }
  28. err:
  29.     gettimeofday(&stop,0);
  30.     printf("dx:%d read_count:%d\n",(stop.tv_sec*1000000+stop.tv_usec) - (start.tv_sec*1000000+start.tv_usec),read_count);
  31.     close(fd);
  32.     if(p)
  33.        free(p);
  34.     return 0;
  35. }
  • //读取的test2文件1.9G
  • [root@localhost my_test]# ls -l -h test2
  • -rw-r--r--. 1 root root 1.9G Sep 21 03:16 test2
  • [root@localhost my_test]gcc -o test_read test_read.c
  • //test_read test是同一个测试程序
  • [root@localhost my_test]#cp test_read test

test和test_read是同一个程序,但是test进程在读取test2文件,在do_generic_file_read()函数中第一次触发文件预读,执行到page_cache_sync_readahead->ondemand_readahead->…->__do_page_cache_readahead()函数,将执行start_async_file_read()函数唤醒async_file_read内核线程。然后在内核线程函数async_file_read_thread()->async_file_read_pages()中,预读test2文件数据到文件页page,一直读取到test2文件结束地址。

test_read读取test2文件时,只有内核默认的文件预读功能,无法触发async_file_read内核线程预读文件。到底test_read和test进程在读取test2文件时,谁耗时长呢?列下测试数据:

  • [root@localhost my_test]# ./test
  • dx:888500 read_count:1997816320
  • [root@localhost my_test]# ./test
  • dx:906958 read_count:1997816320
  • [root@localhost my_test]# ./test
  • dx:867225 read_count:1997816320
  • [root@localhost my_test]# ./test_read
  • dx:1312851 read_count:1997816320
  • [root@localhost my_test]# ./test_read
  • dx:1387640 read_count:1997816320
  • [root@localhost my_test]# ./test_read
  • dx:1306903 read_count:1997816320

可以发现,test读取test2文件仅耗时900ms左右,test_read读取test2文件竟耗时1300ms左右。显然把大部分文件预读放到async_file_read内核线程线程做,主读文件进程性能提升了40%左右。接着,把试程序里的for(i = 0;i < 100;i++) for(j = 0;j < 1000;j++);去除,再看下测试数据

  • [root@localhost my_test]# ./test
  • dx:776955 read_count:1997816320
  • [root@localhost my_test]# ./test
  • dx:648722 read_count:1997816320
  • [root@localhost my_test]# ./test
  • dx:664988 read_count:1997816320
  • [root@localhost my_test]# ./test_read     
  • dx:1067031 read_count:1997816320
  • [root@localhost my_test]# ./test_read
  • dx:1035248 read_count:1997816320

test进程读取test2文件仅耗时600ms左右,test_read进程读取test2文件竟耗时1000ms,性能也提升了不少。并且文件越大,test进程读取文件的耗时少的越明显。

4 问题思考

本文介绍的文件预读优化思路,其实对内核原生代码改动的很少,但发现的问题还是挺多的。

4.1  async_file_read内核线程与主读文件进程的可能冲突

本次优化的思路其实很简单,就是开一个async_file_read内核线程预读文件,只负责把文件数据从磁盘读取到文件页page,不用在其地方花费时间。等主读文件进程read系统调用执行到do_generic_file_read(),有较大大概率本次要读取的文件数据已经读取到了文件页page,就不用再等文件数据从磁盘读取到文件页page了,节省了时间。并且主读文件进程基本很少会再执行do_generic_file_read->page_cache_sync_readahead->ondemand_readahead->…->__do_page_cache_readahead触发文件预读,这个流程原本也是挺花时间的。

但是不得不考虑一种极端情况,async_file_read内核线程因为本地cpu中断频繁、调度延迟等原因,导致async_file_read内核线程运行缓慢。这样就可能出现一种情况,async_file_read内核线程和主读文件进程可能会同时针对同一个索引的page分配struct  page结构,然后是否可能会把两个page都添加到radix tree?

我们细细说下这个问题,主读文件进程执行do_generic_file_read->page_cache_sync_readahead->ondemand_readahead->…->__do_page_cache_readahead触发文件预读

  1. static int
  2. __do_page_cache_readahead(struct address_space *mapping, struct file *filp,
  3.                         pgoff_t offset, unsigned long nr_to_read,
  4.                         unsigned long lookahead_size)
  5. {
  6.     .............
  7.     for (page_idx = 0; page_idx < nr_to_read; page_idx++) {
  8.             pgoff_t page_offset = offset + page_idx;
  9.             if (page_offset > end_index)
  10.                     break;
  11.             rcu_read_lock();
  12.             //radix tree中按照索引index查找page,如果找到提前结束本轮查找,进行下一个索引的page的查找。否则下边执行page_cache_alloc_readahead针对index分配新的page
  13.             page = radix_tree_lookup(&mapping->page_tree, page_offset);
  14.             rcu_read_unlock();
  15.             if (page && !radix_tree_exceptional_entry(page))
  16.                     continue;
  17.             page = page_cache_alloc_readahead(mapping);
  18.             if (!page)
  19.                     break;
  20.             page->index = page_offset;
  21.           if (ret)//这里执行文件系统read接口真正读取文件
  22.              read_pages(mapping, filp, &page_pool, ret);
  23.         ………………
  24.     }
  25. }  

接着执行read_pages->ext4_readpages->mpage_readpages函数

  1. int mpage_readpages(struct address_space *mapping, struct list_head *pages,
  2.                 unsigned nr_pages, get_block_t get_block)
  3. {
  4.     ...........
  5.     for (page_idx = 0; page_idx < nr_pages; page_idx++) {
  6.         struct page *page = list_entry(pages->prev, struct page, lru);       
  7.         //page按照索引index添加到radix tree
  8.         if (!add_to_page_cache_lru(page, mapping,
  9.                     page->index, GFP_KERNEL)) {
  10.             bio = do_mpage_readpage(bio, page,.......);
  11.         }
  12.         page_cache_release(page);
  13.     }
  14. }

显然,__do_page_cache_readahead()函数里按照索引分配的struct page结构,在mpage_readpages()函数将page结构添加到radix tree。

问题来了,主读文件进程预读文件流程是do_generic_file_read->page_cache_sync_readahead->ondemand_readahead->…->__do_page_cache_readahea->read_pages(),async_file_read内核线程预读文件流程是async_file_read_thread->async_file_read_pages->read_pages()。我们举个例子,可能存在这样一种情况,两个进程都针对索引1000的文件页page分配了struct page结构。然后同时执行到read_pages->ext4_readpages->mpage_readpages函数,执行add_to_page_cache_lru()把索引是1000的两个不同的struct page结构添加到radix tree,这是不是有page文件页内存泄漏的风险?

这个问题纠结了很长时间,又想到,如果两个进程同时读相同的文件,执行到do_generic_file_read->page_cache_sync_readahead->ondemand_readahead->…->__do_page_cache_readahea->read_pages(),也是有可能将同一个索引的page添加到radix tree的,那岂不是也有page文件页内存泄漏的风险?内核不应该犯这种低级错误,应该那里做了防护!

看了radix tree的源码后发现了端倪。看下把page文件页添加到radix tree流程:add_to_page_cache_lru->__add_to_page_cache_locked,看下他们的源码

  1. int add_to_page_cache_locked(struct page *page, struct address_space *mapping,
  2.         pgoff_t offset, gfp_t gfp_mask)
  3. {
  4.     .........
  5.     page->mapping = mapping;
  6.     page->index = offset;
  7.     //spin上锁,保证只有一个进程拿到spin
  8.     spin_lock_irq(&mapping->tree_lock);
  9.     error = page_cache_tree_insert(mapping, page, shadowp);                                                         
  10.     if (likely(!error)) {//page添加成功
  11.             __inc_zone_page_state(page, NR_FILE_PAGES);
  12.             spin_unlock_irq(&mapping->tree_lock);
  13.             trace_mm_filemap_add_to_page_cache(page);
  14.     } else {//page添加失败则释放page结构
  15.             page->mapping = NULL;
  16.             spin_unlock_irq(&mapping->tree_lock);
  17.             mem_cgroup_uncharge_cache_page(page);
  18.             page_cache_release(page);
  19.     }
  20. }
  21. static int page_cache_tree_insert(struct address_space *mapping,
  22.                                   struct page *page, void **shadowp)
  23. {
  24.     struct radix_tree_node *node;
  25.     void **slot;
  26.     int error;
  27.     //按照page索引找到该pageradix tree中槽位,slot指向这个槽位
  28.     error = __radix_tree_create(&mapping->page_tree, page->index, 0,
  29.                                 &node, &slot);
  30.     if (error)
  31.             return error;
  32.     if (*slot) {//不为NULL,说明已经有同样索引的page添加到radix tree
  33.             void *p;
  34.             p = radix_tree_deref_slot_protected(slot, &mapping->tree_lock);
  35.             if (!radix_tree_exceptional_entry(p))
  36.                     //已经有同样索引的page添加到radix tree,返回-EEXIST
  37.                     return -EEXIST;
  38.     }          

}                   

源码注释的比较清楚,add_to_page_cache_locked()函数的spin_lock_irq(&mapping->tree_lock)保证同一时间只有一个进程拿到mapping->tree_lock锁,每个文件独一个。并且,如果已经有同样的索引的page添加到radix tree,page_cache_tree_insert()返回-EEXIST,add_to_page_cache_locked()函数则执行page_cache_release(page)释放掉这个page结构。

总结,多个进程即便会针对同一个文件索引index都分配page结构,但是只有一个进程会把该page成功添加到radix tree,后者因为向radix tree添加同样索引的page失败而释放掉page结构。有了这个分析,再也不用担心async_file_read内核线程预读文件页page时,会与主读文件进程有什么冲突,甩开膀子使劲读文件就行了!内核已经做了做了足够的防护。

4.2  page文件页预读标记的思考

我们知道内核原生预读代码,执行到__do_page_cache_readahead()函数,会对预读的第一个文件页执行SetPageReadahead(page)加上“预读标记”。 后续do_generic_file_read()函数中,会执行if (PageReadahead(page))判断当前读取的文件页page是否有预读标记,有的话就执行page_cache_async_readahead()发起异步文件预读。

那么问题来了,如果async_file_read内核线程已经把大部分文件数据读取到了文件页page,此时主读文件进程在do_generic_file_read()函数中,执行if (PageReadahead(page))判断当前读取的文件页page有预读标记,就要执行page_cache_async_readahead()发起异步文件预读。有点担心后续会发生不能预料的问题?想想有两种可能:

1:在page_cache_async_readahead-> ondemand_readahead函数中,if (hit_readahead_marker)成立,执行page_cache_next_hole()函数查找第一个hole page,但是hole page索引太大,导致if (!start || start - offset > max)成立,直接return 0,提前结束预读。没有啥问题!

2:page_cache_next_hole()成功找到了hole page,然后执行到__do_page_cache_readahead()函数,因为在radix tree中找不到对应索引的page结构而分配新的page结构,然后执行read_pages()将对应文件数据读取到文件页page。在前一节的基础上,这里说明另外一点,两个进程同时执行__do_page_cache_readahead-> read_pages()读取文件数据到文件页page,是完全不会有冲突的!所以,该怎么做就怎么做,文件页page预读标记位没啥影响。

4.3  async_file_read内核线程的休眠与唤醒

这点比较水,主要是担心start_async_file_read ()函数先执行wake_up_process(async_file_read.task)唤醒async_file_read内核线程。然后async_file_read内核线程执行schedule()休眠,是不是会导致该线程错过被唤醒的机会。不会,因为wake_up_process()会检测简称待唤醒的进程on_cpu标记位是否为1,为1则休眠。等进程执行schedule()完全休眠才会on_cpu=0。此时wake_up_process()才能继续执行下边的代码,去唤醒目标进程,所以也没问题。

另外一点是进程需要S状态后执行schedule()休眠,不能D状态。否则内核认为这是进程D异常,会打印120s进程hung检测报错。

5 后续

主要是后续的优化与测试,期望更稳定,性能提升更高。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值