之前写过一篇文章《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()源码简单列下:
- //内核文件读取核心函数
- static void do_generic_file_read(struct file *filp, loff_t *ppos,
- read_descriptor_t *desc, read_actor_t actor)
- {
- .............
- for (;;) {
- ..........
- //根据本次读取的文件逻辑地址获取映射的page文件页
- page = find_get_page(mapping, index);
- if (!page) {
- //如果找不到page文件页则触发同步文件预读
- page_cache_sync_readahead(mapping,
- ra, filp,
- index, last_index - index);
- ............
- }
- //如果本次读取page文件页有预读标记位,触发异步文件预读
- if (PageReadahead(page)) {
- page_cache_async_readahead(mapping,
- ra, filp, page,
- index, last_index - index);
- }
- //如果page文件页对应的磁盘文件数据还没有读取到page文件页内存
- if (!PageUptodate(page)) {
- ................
- //尝试获取page锁失败而休眠,等对应的磁盘文件数据读取到page文件页,再把当前进程唤醒
- if (!trylock_page(page))
- goto page_not_up_to_date;
- ...............
- unlock_page(page);
- }
- }
- .............
- }
第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,列下源码:
- 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)
- {
- .............
- for (page_idx = 0; page_idx < nr_to_read; page_idx++) {
- pgoff_t page_offset = offset + page_idx;
- if (page_offset > end_index)
- break;
- rcu_read_lock();
- page = radix_tree_lookup(&mapping->page_tree, page_offset);
- rcu_read_unlock();
- if (page && !radix_tree_exceptional_entry(page))
- continue;
- page = page_cache_alloc_readahead(mapping);
- if (!page)
- break;
- page->index = page_offset;
- list_add(&page->lru, &page_pool);
- if (page_idx == nr_to_read - lookahead_size)
- SetPageReadahead(page);
- ret++;
- }
- if (ret) //执行文件系统read接口真正读取文件数据到page文件页
- read_pages(mapping, filp, &page_pool, ret);
- //page_idx+offset是已经预读的最后一个page文件页索引加1,end_index表示文件结束地址对应的page文件页索引。如果page_idx+offset <= end_index成立,说明还有文件数据没有读取,则执行start_async_file_read()继续触发预读(本文介绍的预读优化)
- if(page_idx + offset <= end_index)
- //从page_idx + offset这个索引的page开始预读
- start_async_file_read(mapping, filp,page_idx + offset);
- ........
- }
在__do_page_cache_readahead()函数里添加了红色那两行代码,主要是执行start_async_file_read()唤醒文件预读内核线程async_file_read。这样在读取文件时,第一次触发内核默认预读,执行到__do_page_cache_readahead()函数,就会执行到start_async_file_read()函数。
- struct async_file_read_info{
- struct task_struct *task;//保存预读内核线程async_file_read的进程结构
- struct address_space *mapping;//预读的文件struct address_space结构
- struct file *filp;//预读的文件struct file结构
- unsigned long start_read_page;//预读的文件的起始文件页索引
- atomic_t count;//目前控制同时只有一个进程能触发预读内核线程async_file_read
- };
- struct async_file_read_info async_file_read;
- static int start_async_file_read(struct address_space *mapping,struct file *filp,unsigned long start_read_page)
- {
- if(strcmp(current->comm,"test") == 0 || strcmp(current->comm,"cat") == 0)
- {
- if(async_file_read.task == NULL){
- async_file_read.task = kthread_create(async_file_read_thread,(void *)&async_file_read,"async_file_read");
- if (IS_ERR(async_file_read.task)) {
- printk("%s kthread_create fail\n",__func__);
- return -1;
- }
- }
- //sync_file_read.count为0成立并加1,说明这是第一个触发async_file_read的进程,目前支持一个进程async_file_read
- if(atomic_add_return(1,&async_file_read.count) == 1){
- async_file_read.mapping = mapping;
- async_file_read.filp = filp;
- async_file_read.start_read_page = start_read_page;
- //唤醒进程,只支持单个进程async_file_read
- wake_up_process(async_file_read.task);
- }else{
- atomic_dec(&async_file_read.count);
- }
- }
- return 0;
- }
首先定义了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源码:
- static int async_file_read_thread(void *arg)
- {
- struct async_file_read_info *async_file_read_tmp = (struct async_file_read_info *)arg;
- while (!kthread_should_stop()) {
- //开始读取文件
- async_file_read_pages(async_file_read_tmp);
- if(atomic_read(&async_file_read_tmp->count) > 0)
- atomic_dec(&async_file_read_tmp->count);
- else
- printk("%s %s %d sync_file_read.count:%d\n",__func__,current->comm,current->pid,atomic_read(&async_file_read_tmp->count));
- //设置进程S状态
- set_current_state(TASK_INTERRUPTIBLE);
- //休眠
- schedule();
- __set_current_state(TASK_RUNNING);
- }
- async_file_read_tmp->task = NULL;
- return 0;
- }
当async_file_read内核线程被唤醒,就会执行它的线程函数async_file_read_thread()。该函数执行async_file_read_pages(async_file_read_tmp)真正预读文件。预读完执行schedule()休眠,循环往复。async_file_read_pages()函数源码如下:
- static int async_file_read_pages(struct async_file_read_info * p_async_file_read_info)
- {
- struct address_space *mapping = p_async_file_read_info->mapping;
- struct file *filp = p_async_file_read_info->filp;
- pgoff_t page_to_read,start_read_page,page_offset;
- struct inode *inode = mapping->host;
- struct page *page;
- unsigned long end_index; /* The last page we want to read */
- LIST_HEAD(page_pool);
- int page_idx;
- int ret = 0;
- loff_t isize = i_size_read(inode);
- if (isize == 0)
- return -1;
- //结束文件页索引
- end_index = ((isize - 1) >> PAGE_CACHE_SHIFT);
- //预读的起始page文件页索引
- start_read_page = p_async_file_read_info->start_read_page;
- //预读的page文件页数量,默认一直读到文件结束地址的page文件页
- page_to_read = end_index - p_async_file_read_info->start_read_page;
- for (page_idx = 0; page_idx < page_to_read; page_idx++){
- page_offset = start_read_page + page_idx;
- if (page_offset > end_index)
- break;
- rcu_read_lock();
- //先在radix tree按照page文件页索引查找struct page结构
- page = radix_tree_lookup(&mapping->page_tree, page_offset);
- rcu_read_unlock();
- if (page && !radix_tree_exceptional_entry(page))
- continue;
- //没有找到page结构则根据page文件页索引page_offset分配struct page结构
- page = page_cache_alloc_readahead(mapping);
- if (!page)
- break;
- page->index = page_offset;
- //把page结构添加到page_pool链表
- list_add(&page->lru, &page_pool);
- //ret是预读的page文件页数
- ret++;
- }
- if (ret)//执行文件系统read接口真正读取文件数据到page文件页
- read_pages(mapping, filp, &page_pool, ret);
- return 0;
- }
可以发现,源码与内核原有的文件预读函数__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,省略出错判断代码。
- #define FILE_SIZE (1024*1024*1)
- int main(int argc,char *argv[ ])
- {
- int ret = 0;
- unsigned char *p;
- int fd;
- int i,j;
- struct timeval start,stop;
- int read_count = 0;
- p = (unsigned char*)malloc(FILE_SIZE);
- …………
- //释放pagecache,避免对测试数据有影响
- system("echo 1 >/proc/sys/vm/drop_caches");
- fd = open("test2",O_RDWR|O_SYNC);
- …………
- gettimeofday(&start,0);
- while(1)
- {
- ret = read(fd,p,FILE_SIZE);
- if(ret > 0)
- read_count += ret;
- if(ret <= 0)
- goto err;
- //for循环模拟对读取到的数据做一定处理
- for(i = 0;i < 100;i++)
- for(j = 0;j < 1000;j++);
- }
- err:
- gettimeofday(&stop,0);
- printf("dx:%d read_count:%d\n",(stop.tv_sec*1000000+stop.tv_usec) - (start.tv_sec*1000000+start.tv_usec),read_count);
- close(fd);
- if(p)
- free(p);
- return 0;
- }
- //读取的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触发文件预读
- 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)
- {
- .............
- for (page_idx = 0; page_idx < nr_to_read; page_idx++) {
- pgoff_t page_offset = offset + page_idx;
- if (page_offset > end_index)
- break;
- rcu_read_lock();
- //radix tree中按照索引index查找page,如果找到提前结束本轮查找,进行下一个索引的page的查找。否则下边执行page_cache_alloc_readahead针对index分配新的page
- page = radix_tree_lookup(&mapping->page_tree, page_offset);
- rcu_read_unlock();
- if (page && !radix_tree_exceptional_entry(page))
- continue;
- page = page_cache_alloc_readahead(mapping);
- if (!page)
- break;
- page->index = page_offset;
- if (ret)//这里执行文件系统read接口真正读取文件
- read_pages(mapping, filp, &page_pool, ret);
- ………………
- }
- }
接着执行read_pages->ext4_readpages->mpage_readpages函数
- int mpage_readpages(struct address_space *mapping, struct list_head *pages,
- unsigned nr_pages, get_block_t get_block)
- {
- ...........
- for (page_idx = 0; page_idx < nr_pages; page_idx++) {
- struct page *page = list_entry(pages->prev, struct page, lru);
- //page按照索引index添加到radix tree
- if (!add_to_page_cache_lru(page, mapping,
- page->index, GFP_KERNEL)) {
- bio = do_mpage_readpage(bio, page,.......);
- }
- page_cache_release(page);
- }
- }
显然,__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,看下他们的源码
- int add_to_page_cache_locked(struct page *page, struct address_space *mapping,
- pgoff_t offset, gfp_t gfp_mask)
- {
- .........
- page->mapping = mapping;
- page->index = offset;
- //spin上锁,保证只有一个进程拿到spin锁
- spin_lock_irq(&mapping->tree_lock);
- error = page_cache_tree_insert(mapping, page, shadowp);
- if (likely(!error)) {//page添加成功
- __inc_zone_page_state(page, NR_FILE_PAGES);
- spin_unlock_irq(&mapping->tree_lock);
- trace_mm_filemap_add_to_page_cache(page);
- } else {//page添加失败则释放page结构
- page->mapping = NULL;
- spin_unlock_irq(&mapping->tree_lock);
- mem_cgroup_uncharge_cache_page(page);
- page_cache_release(page);
- }
- }
- static int page_cache_tree_insert(struct address_space *mapping,
- struct page *page, void **shadowp)
- {
- struct radix_tree_node *node;
- void **slot;
- int error;
- //按照page索引找到该page在radix tree中槽位,slot指向这个槽位
- error = __radix_tree_create(&mapping->page_tree, page->index, 0,
- &node, &slot);
- if (error)
- return error;
- if (*slot) {//不为NULL,说明已经有同样索引的page添加到radix tree了
- void *p;
- p = radix_tree_deref_slot_protected(slot, &mapping->tree_lock);
- if (!radix_tree_exceptional_entry(p))
- //已经有同样索引的page添加到radix tree,返回-EEXIST
- return -EEXIST;
- }
}
源码注释的比较清楚,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 后续
主要是后续的优化与测试,期望更稳定,性能提升更高。