服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

Linux Readahead 预读机制分析(Linux 4.14)

日期: 来源:Android Performance收集编辑:Vier

一、基本概念

设计背景

文件访问类型一般是顺序的,访问[A, B]范围的数据后,接下来很可能访问[B+1, B+N]数据。由于访问磁盘、flash等存储器件比较耗时,在访问 [A, B]的时候,如果提前把[B+1, B+N]数据从存储器件读取到ram中,那么后继需要用[B+1, B+N]数据时,就不需要通过耗时的disk io从存储器件读取数据了,从而提高性能。

预读带来的好处

对于顺序读,适量的预读可以提升性能。原因如下:

1)提前读取数据,避免耗时的disk io读取数据

预读数据缓存在page cache中(struct file->f_mapping->page_tree),用的时候,直接从缓存读取,不需要从存储器件读取数据(这个操作耗时)。

2)提高存储栈、器件处理效率

预读本质是提前读取即将访问的数据,以备将来使用,这意味着“当前数据”和“预读数据”的逻辑地址是连续的,这两个io请求在内核存储栈中(filesytem-->block layer-->device)就可以进行合并(merge)处理。处理一个io请求,即可满足原先的两个io请求需求。

3)降低硬中断、软中断带来的负载

器件处理io请求完成时,内核通过硬中断、软中断处理io完成时的工作,合并io请求,可降低中断数量,减少中断带来的额外负载。

4)避免存储栈拥塞导致read响应不及时

器件的io数量是有上限的(默认值/sys/block/sdc/queue/nr_requests=128),当器件待处理的io数超过7/8 * nr_requests时,存储栈进入拥塞状态并限制生成io请求的速度;当待处理的io请求数达到最大值nr_requests时,read因申请不到struct request而阻塞等待,直至一些io请求处理完后被存储栈回收,read、write才能转换成io请求,然后提交io请求给器件处理。合并io请求,可减少io数量,降低存储栈拥塞的概率。

5)避免磁盘磁头来回移动

这一点是针对机械硬盘的。文件数据[A,B]和[B+1, B+N]在逻辑地址上是连续的,磁头只需往一个方向移动。作为对比,访问[A,B],[A-N, A-1],[B+1, B+N],就会出现磁头来回移动的情况,磁头的机械运动是很耗时的。

预读带来的坏处

1)对于随机读,预读的数据不会被用到,白白占用了存储器件带宽及系统内存。

2)大量预读可能导致io负载增大。

3)大量预读可能导致内存压力增大。

同步预读 & 异步预读

源码中有两个函数page_cache_sync_readahead和page_cache_async_readahead,分别称作同步预读、异步预读。这个地方容易让人迷糊,需解释一下二者的区别。

同步预读:从存储器件读取多个page数据,一些page给read使用,一些page留给将来使用。在下图,read文件数据时,根据page->index在page cache中(struct file->f_mapping->page_tree)找需要的页面(蓝色页面),如果找不到就执行同步预读page_cache_sync_readahead。同步预读根据待读数据的逻辑地址、待读page数量及地址信息,封装成bio,然后调用submit_bio提交给器件处理。不过要注意预读提交bio后就返回了,并不会等待page中的数据变成PageUptodate。

异步预读:本次读的page纯粹是为将来准备的,触发异步预读的read目前不需要这些数据。这些page提前从存储器件读取并将page加入到page cache中。

二、关键数据结构及原理

数据结构

预读的核心数据结构是struct file_ra_state,定义在linux/fs.h中:

/* * Track a single file's readahead state *//* 这个结构体是struct file相关的,描述了file的预读状态。预读按预读窗口推进,预读窗口中含有file_ra_state->size个page。   read访问预读窗口中的page,访问到第size-async_size个page,也   即预读窗口中还剩async_size个page没有访问时,启动异步预读读入一   批新的page作为预读窗口 */struct file_ra_state {    //从start这个page开始读    pgoff_t start;            /* where readahead started *///读取size个page放入预读窗口。如内存不足无法申请page,则预读小于size个page    unsigned int size;        /* # of readahead pages */    //预读窗口中还剩async_size个page时,启动异步预读    unsigned int async_size;    /* do asynchronous readahead when                       there are only # of pages ahead */    //预读窗口上限值(单位: page).      默认等于struct backing_dev_info->ra_pages, 可通过fadvise调整。如果read需要读的page数量小于ra_pages,最多读取ra_pages个页面.      如果read需要读的page数量大于ra_pages,最多读取 min { read的page数量,存储器件单次io最大page数量 }个页面.      预读窗口中当前有多少个页面由size成员变量表示.    unsigned int ra_pages;        /* Maximum readahead window */    unsigned int mmap_miss;        /* Cache miss stat for mmap accesses */    //最后一次的读位置(单位:字节)    loff_t prev_pos;        /* Cache last read() position */};

关键字段意义如下图:

原理

read请求N 个page数据,通过预读从存储器件读取M个page数据(M > N),并对其中的第a个page设置PageReadahead标记(0 ≤ a < N)。PageReadahead标记起到一个标识作用,表示预读窗口中剩余的page不多了,需要启动异步预读再读入一批page存放page cache。

如果是顺序读,肯定会读到PageReadahead标记的页(单线程读的场景),或者读到预读窗口的最后一个页(多线程穿插读的场景),所以代码作者认为,如果访问到这两种页就是顺序读(注意:这样的判断不是很准,但代码写起来简单高效),否则认为是随机访问。

如果是顺序访问,预读窗口在之前的基础上扩大2倍或者4倍(上限值不超过struct file_ra_state->ra_pages),然后从存储器件读入数据填充这些page,并缓存在page cache中,最后,在新读入的这批page中选定一个page设置PageReadahead标记(一般是新读入的这批page中的第一个page)。

如果是随机访问,预读机制仅从存储器件中读取read函数需要的数据,read请求几个page,就读几个page,并缓存在page cache中。可以看出随机读不会预读多余的page,另外注意随机读不更改预读窗口。

三、预读示例

按 4k 顺序访问文件的[0, 8*4K]的数据(顺序访问),然后lseek到108*4k处访问文件(随机访问)。过程如下:

1)访问第0个page数据

page->index=0的页面在page cache中找不到,触发同步预读page_cache_sync_readahead,一次性读了4个page(read需要1个page,预读3个page。预读page数量与实际请求的page数量、file_ra_state->ra_pages有关,通过get_init_ra_size计算)。本次预读建立的预读窗口如下:

注意,预读窗口中第ra->size - ra->async_size = 1个page,即page->index=1设置了PageReadahead标记。

2)访问第1个page数据

page->index=1的页面在page cache中能找到(预读命中),不需要从存储器件中读取数据。又因该page有PageReadahead标记,触发异步预读page_cache_async_readahead,预读页面数量由get_next_ra_size计算得到,因为本次请求的数据起始位置与上一次读结束位置相同,属于顺序读,get_next_ra_size加大预读量,预读量从之前的4 page增大到8 page。本次预读建立的预读窗口如下:

注意,预读窗口中第ra->size - ra->async_size = 0个page,即page->index=4设置了PageReadahead标记。

3)访问第2、3个page数据

这两个page在page cache中可以找到(预读命中),直接从page cache中读取数据。

4)访问第4个page数据

page->index=4的页面在page cache中可以找到(预读命中),直接从page cache中读取数据。不过这个page设置了PageReadahead标记,触发异步预读page_cache_async_readahead,由于是顺序读,get_next_ra_size将预读量从8 page增大到16 page,本次预读建立的预读窗口如下:

后继的顺序访问流程重复上面过程,遇到page被标记成PageReadahead,增大预读量(最大不超过struct file_ra_state->ra_pages)后启动异步预读。

5)lseek跳到108*4k处访问第108个page数据

访问page->index=108的page,不符合顺序读的条件,所以代码判断成随机读。如果是随机读,则不做预读,read请求几个页的数据,就从存储器件中读几个页数据(ondemand_readahead --> __do_page_cache_readahead)。

注意,这次随机读,不会更改预读窗口状态。

四、关键代码分析

同步预读generic_file_buffered_read --> page_cache_sync_readahead 与 异步预读generic_file_buffered_read --> page_cache_async_readahead都会调用ondemand_readahead,在这个函数中计算预读量,然后ra_submit提交io请求。

/* @offset: 从哪个page开始读    @req_size: 需要读多少个page,这是read需要的数量   如果是顺序读,ondemand_readahead读取至少req_size个page。   如果是随机读,ondemand_readahead仅读取req_szie个page。*/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){  struct backing_dev_info *bdi = inode_to_bdi(mapping->host);  unsigned long max_pages = ra->ra_pages;  unsigned long add_pages;  pgoff_t prev_offset;
/* * If the request exceeds the readahead window, allow the read to * be up to the optimal hardware IO size *//* read请求的page大于预读窗口大小,预读的page数量上调至存储器件单次io最大的page数量 */ if (req_size > max_pages && bdi->io_pages > max_pages) max_pages = min(req_size, bdi->io_pages);
/* * start of file */ if (!offset) goto initial_readahead;
/* * It's the expected callback offset, assume sequential access. * Ramp up sizes, and push forward the readahead window. *//* 条件1:offset == ra->start + ra->size - ra->async_size
/ 同步建立的预读窗口 \ / \           |----------------------------------------|           |    |   |   | ^ |   |   |   |   |   |   |           |--------------|-------------------------|                       | ra->start + ra->size - ra->async_size 该page设置了PageReadahead
上图是同步预读建立的预读窗口,从第0个page到第ra->size - ra->async_size 个page,是read需要用的,后面的page是预留给下次read用的。 读到第ra->size - ra->async_size个pag,表明预读窗口已经开始使用, 需要启动异步预读,读入一批page建立一个新的预读窗口,为下下次read做准备。
条件2:offset == ra->start + ra->size                前一次     /      异步建立的预读窗口      \             预读窗口   /                                \           -----------|-----------------------------------|               |  |  | ^ |  |  |  |  |  |  |  |  |  |  |  |             ------------|-----------------------------------|                     | ra->start + ra->size(前一次预读用完了,执行下一个预读窗口的第一个page) 该page设置了PageReadahead
异步预读建立的预读窗口中,第一个page标记为PageReadahead,访问到这个标记的 页面,表明新预读窗口已经开始使用,需要启动异步预读,读入一批page建立一个新的 预读窗口,为下下次read做准备。
满足这两个条件,可认为本次read预上一次read是顺序读类型 (见原理部分描述)。 这个条件是不准确的,但是代码实现简单高效。*/ if ((offset == (ra->start + ra->size - ra->async_size) || offset == (ra->start + ra->size))) { ra->start += ra->size; /*既然是顺序读,就扩大预读窗口: 如果当前预读窗口大小ra->size < 1/16 * 允许读的最多page数 1)则预读窗口在原基础上扩大4倍。否则扩大2倍。 2)扩大后的窗口大小,不能超过“允许读的最多page数量”*/ ra->size = get_next_ra_size(ra, max_pages); ra->async_size = ra->size; goto readit; }
/* * Hit a marked page without valid readahead state. * E.g. interleaved reads. * Query the pagecache for async_size, which normally equals to * readahead size. Ramp it up and use it as the new readahead size. *//* 多线程顺序读一个文件(interleaved read),破坏了file->ra_state,所以没法根据前面的条件 判断出是顺序读类型。如果是顺序读,最终一定会访问到预读窗口中的PageReadahead标记的page。 所以,反过来推,访问到PageReadahead标记的page,就认为是顺序读。当然这样的判断是不准确的, 但准确率高,代码简单。*/ if (hit_readahead_marker) { pgoff_t start;
/* offset + 1开始,找到第一个不在page cache中的文件数据页,作为预读的开始 */ rcu_read_lock(); start = page_cache_next_hole(mapping, offset + 1, max_pages); rcu_read_unlock();
if (!start || start - offset > max_pages) return 0;
ra->start = start; ra->size = start - offset; /* old async_size */ ra->size += req_size; ra->size = get_next_ra_size(ra, max_pages); ra->async_size = ra->size; goto readit; }
/* * oversize read */ if (req_size > max_pages) goto initial_readahead;
/* * sequential cache miss * trivial case: (offset - prev_offset) == 1 * unaligned reads: (offset - prev_offset) == 0 *//* 上面的场景根据下面3个条件判断是否是顺序读: offset == ra->start + ra->size - ra->async_size offset == ra->start + ra->size hit_readahead_marker 代码执行到这里,属于随机读场景。*/ prev_offset = (unsigned long long)ra->prev_pos >> PAGE_SHIFT; if (offset - prev_offset <= 1UL) goto initial_readahead;
/* * Query the page cache and look for the traces(cached history pages) * that a sequential stream would leave behind. */ if (try_context_readahead(mapping, ra, offset, req_size, max_pages)) goto readit;
/* * standalone, small random read * Read as is, and do not pollute the readahead state. *//* read请求req_size,__do_page_cache_readahead只读req_size个page。注意2点: 1)__do_page_cache_readahead-->read_pages条件io就返回了,不会等待page变成PageUptodate。 2)__do_page_cache_readahead不会更改 struct file_ra_state。*/ return __do_page_cache_readahead(mapping, filp, offset, req_size, 0);
initial_readahead:/* 第一次读文件,初始化预读窗口 */ ra->start = offset; ra->size = get_init_ra_size(req_size, max_pages); ra->async_size = ra->size > req_size ? ra->size - req_size : ra->size;
readit: /* * Will this read hit the readahead marker made by itself? * If so, trigger the readahead marker hit now, and merge * the resulted next readahead window into the current one. * Take care of maximum IO pages as above. *//* hit_readahead_marker预期建立了一个预读窗口A,待访问offset刚好是这个预读窗口的第一个page, 这说明访问A窗口,后面没有预留的预读窗口了,需建立下一个预读窗口B备用,既然A、B都还没预读, 就合并后一起预读。*/ if (offset == ra->start && ra->size == ra->async_size) { add_pages = get_next_ra_size(ra, max_pages); if (ra->size + add_pages <= max_pages) { ra->async_size = add_pages; ra->size += add_pages; } else { ra->size = max_pages; ra->async_size = max_pages >> 1; } }
/* ra_submit --> __do_page_cache_readahead尽量分配ra->size个page, 分配失败就停止分配,这些page加入page_pool链表。
read-->page_cache_sync_readahead,假设read需要读取nr_to_read个page, 预读M个page,则第nr_to_read个page(从0开始计数)设置PageReadahead标记。
read-->page_cache_async_readahead,则预读的第0个page(从0开始计数)设置 PageReadahead标记。
ra_submit -->__do_page_cache_readahead --> read_pages调用 mapping->a_ops->readpages或者mapping->a_ops->readpage依次读入page_pool 链表中的page。
ra_submit提交io后就返回,不会等待page变成PageUptodate。*/ return ra_submit(ra, mapping, filp);}

五、代码待优化的地方

场景

generic_file_buffered_read -->page_cache_sync_readahead或page_cache_async_readahead --> ondemand_readahead --> ra_submit --> __do_page_cache_readahead 会把刚刚申请的一批page(用于预读)加入到page cache中,如果读失败了,这些page并不会从page cache中清除,只是标记成PageError。

问题

generic_file_buffered_read -->find_get_page到page,但不是PageUptodate状态,则认为io错误导致的问题,就通过mapping->a_ops->readpage重新读取单个page数据。所以预读发生io err后,后继read不会批量读入这些page了,只会单个page读,性能较差。

解决

对于这个问题,5.18内核版本已经修复https://lkml.org/lkml/2022/2/10/14。

相关阅读

  • 预期的兜兜转转

  • 特别声明本公号坚持第三方独立研究平台的市场定位,积极响应并贯彻国家政策导向,助力国家高质量发展战略,致力于向读者传递正能量,分享前沿君团队及市场优质研究成果,搭建互动交流
  • MySQL 切换 ShardingSphere 的测试实践

  • 作者:曾哲宇 编辑:毕小烦应用项目作为 toB 基础授权系统,其特点是数据访问量大,单表数据量大,数据热度高。目前的数据库采用单点 MySQL 实例形式,虽当前阶段仅接入少量业务,但其数
  • Qt完结,后续安排

  • t终于完结了,共计63个视频。正式调价为60元(55元+5元流量费)。录制一小段视频,说明下后续的课程设想:1)千呼万唤的数据结构将再次开始,这次一定完结不再托更。还剩下,《查找》,《排序
  • 远望资本田鸿飞:链上声誉行业分析

  • 在《DID行业研究报告》的普及和铺垫之后,我们重点来分析一下链上声誉行业。Photo by Ben Sweet on Unsplash互联网的发展从90年代的“在互联网上你不知道对方是条狗”的匿名
  • 远望资本程浩:以太坊会垄断公链吗?

  • 大家都过了非常不容易的一年,展望已经到来的2023年,浩哥谈几点信心。同时浩哥最近投入大量时间研究了Web3和Crypto加密世界,有一些个人思考,希望在这开年时刻抛砖引玉Photo by Z
  • 新年重磅 | 2022全球数据合规年度大事件回顾

  • 2022年,是中国数据合规发展快速迅猛的一年。《中华人民共和国个人信息保护法》、《中华人民共和国数据安全法》正式实施一年,配套的相关法规政策及制度规范正在逐步落地实施,并
  • B端产品,如何优雅地实现「数据批量导入」

  • 关注并将「人人都是产品经理」设为星标每天早 07 : 45 按时送达数据批量导入,是B端产品中常常会遇到的场景,设计得好,则能够加快产品的落地实施过程、提高客户的工作效率。作者

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • Linux Readahead 预读机制分析(Linux 4.14)

  • 一、基本概念设计背景文件访问类型一般是顺序的,访问[A, B]范围的数据后,接下来很可能访问[B+1, B+N]数据。由于访问磁盘、flash等存储器件比较耗时,在访问 [A, B]的时候,如果提
  • Jetpack 更新一览 | 2022 Android 开发者峰会

  • 作者 / 产品经理 Amanda AlexanderAndroid Jetpack 是现代 Android 开发 (Modern Android Development) 的关键组件。Android Jetpack 套件包含超过 100 个库、工具及指南,可
  • 对父母的印象,是否定格在自己中学时期?

  • 人生的旅途中,难免会离开父母无论是外出求学,还是去大城市奋斗都减少了与父母相聚的时光我们对父母最深的印象也仿佛定格在了中学时期……2022年,我做了适老化项目,前后和团队访
  • 阳转阴了,记录一下和病毒两次团战失利的全过程

  • 为何记录?这一次感染,个人认为有几个点比较有代表性,又恰逢政策放开的当口,在每个人都会被感染的情况下,给大家提供一些参考:被传染的时间和地点很明确,方便大家计算潜伏期:因为之前
  • 梅西夺冠,一场精彩的比赛,一个时代的落幕

  • 当然,对梅西来说,是个美好的落幕,梅球王终于不再是无冕之王!​看到朋友圈有小伙伴发:“06年德国世界杯,天才少年梅西横空出世,但是因为主教太保守,在1:0领先德国后换下了里克尔梅,导
  • 再见 2022,你好 2023

  • 感谢公众号的各位读者陪伴我度过 2022,感谢大家平时的支持和鼓励!只希望大家 2023 年身体健康吧,其他的都是过眼浮云,身体才是革命的本钱。年末因为血氧太低(血氧仪和手表自测都