linux文件读取中的Readahead预读机制

Readahead预读机制

由于内存的速度比磁盘速度快很多,如果每一次访问文件数据都要从磁盘读取一次数据,就会导致非常严重的时延。因此Linux为了提高性能,通过page cache机制,将多个用户数据缓存在内存当中,从而避免多次再磁盘读取。Readahead预读机制正是将用户数据缓存到内存的方法之一。

Readahead机制的介绍

Readahead预读机制是Linux针对顺序读的性能优化机制。它的核心思想是当用户访问连续多个page的时候,一次性将多个连续的页从磁盘读取到内存中,从而避免多次与磁盘交互,降低性能。

Readahead机制的源码分析

预读行为的在generic_file_buffered_read函数做了具体实现,如下:

static ssize_t generic_file_buffered_read(struct kiocb *iocb,
        struct iov_iter *iter, ssize_t written)
{
    ...
    for (;;) {
        page = find_get_page(mapping, index); // 访问特定index的page
        if (!page) { // 如果是NULL,即在page cache找不到
            page_cache_sync_readahead(mapping,
                    ra, filp,
                    index, last_index - index); // 进行同步预读
            page = find_get_page(mapping, index); // 预读以后再获取一次
        }
        if (PageReadahead(page)) { // 如果读取出来的page包含Readahead的特殊标志
            page_cache_async_readahead(mapping,
                    ra, filp, page,
                    index, last_index - index); // 进行一次异步预读
        }
    }
    ...
}

从上面可以知道,当find_get_page函数无法在page cache中找到该index对应的page的实例,就会调用page_cache_sync_readahead函数进行同步预读,预读完的数据就会加入到page cache中,再一次调用find_get_page函数就可以获取出来。

page index=1的页以及异步预读出来的最后一个页会包含特殊标志,这个标志可以通过PageReadahead函数进行判断,从而判断是否需要调用page_cache_async_readahead进行异步预读。

同步预读以及异步预读都会调用同一个函数ondemand_readahead,只是输入参数不一样,如下所示:

void page_cache_sync_readahead(struct address_space *mapping,
                   struct file_ra_state *ra, struct file *filp,
                   pgoff_t offset, unsigned long req_size)
{
    if (!ra->ra_pages)
        return;

    if (blk_cgroup_congested())
        return;

    if (filp && (filp->f_mode & FMODE_RANDOM)) {
        force_page_cache_readahead(mapping, filp, offset, req_size);
        return;
    }

    ondemand_readahead(mapping, ra, filp, false, offset, req_size);
}
page_cache_async_readahead(struct address_space *mapping,
               struct file_ra_state *ra, struct file *filp,
               struct page *page, pgoff_t offset,
               unsigned long req_size)
{
    if (!ra->ra_pages)
        return;

    if (PageWriteback(page))
        return;

    ClearPageReadahead(page); // 如前面所述,异步预读的最后一个页会特殊的标志,再一次触发异步预读后在这里清除标志

    if (inode_read_congested(mapping->host))
        return;

    if (blk_cgroup_congested())
        return;

    ondemand_readahead(mapping, ra, filp, true, offset, req_size);
}

根据代码,同步预读和异步预读传入ondemand_readahead函数的第四个参数不一样,一个是False一个是True。为了搞清楚差异,继续分析ondemand_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)
{
    struct backing_dev_info *bdi = inode_to_bdi(mapping->host);
    unsigned long max_pages = ra->ra_pages; // 一般值是32
    unsigned long add_pages;
    pgoff_t prev_offset;
    
    /*
     * 根据条件,计算本次预读最大预读取多少个页,一般情况下是max_pages=32个页
     */
    if (req_size > max_pages && bdi->io_pages > max_pages)
        max_pages = min(req_size, bdi->io_pages);
    /*
     * offset即page index,如果page index=0,表示这是文件第一个页,跳转到initial_readahead进行处理
     */
    if (!offset)
        goto initial_readahead;

    /*
     * 默认情况下是 ra->start=0, ra->size=0, ra->async_size=0 ra->prev_pos=0
     * 但是经过第一次预读后,上面三个值会出现变化
     */
    if ((offset == (ra->start + ra->size - ra->async_size) ||
         offset == (ra->start + ra->size))) {
        ra->start += ra->size;
        ra->size = get_next_ra_size(ra, max_pages);
        ra->async_size = ra->size;
        goto readit;
    }

    /*
     * 异步预读的时候会进入这个判断,更新ra的值,然后预读特定的范围的页
     * 异步预读的调用表示Readahead出来的页连续命中
     */
    if (hit_readahead_marker) {
        pgoff_t start;

        rcu_read_lock();
        // 这个函数用于找到offset + 1开始到offset + 1 + max_pages这个范围内,第一个不在page cache的页的index
        start = page_cache_next_miss(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;
        /* 
         * 由于连续命中,get_next_ra_size会加倍上次的预读页数
         * 第一次预读了4个页
         * 第二次命中以后,预读8个页
         * 第三次命中以后,预读16个页
         * 第四次命中以后,预读32个页,达到默认情况下最大的读取页数
         * 第五次、第六次、第N次命中都是预读32个页
         * */
        ra->size = get_next_ra_size(ra, max_pages);
        ra->async_size = ra->size;
        goto readit;
    }

    if (req_size > max_pages)
        goto initial_readahead;

    prev_offset = (unsigned long long)ra->prev_pos >> PAGE_SHIFT;
    if (offset - prev_offset <= 1UL)
        goto initial_readahead;

    if (try_context_readahead(mapping, ra, offset, req_size, max_pages))
        goto readit;
    // 这个函数执行具体的从磁盘读取的流程
    return __do_page_cache_readahead(mapping, filp, offset, req_size, 0);

initial_readahead:
    ra->start = offset;
    /* get_init_ra_size初始化第一次预读的页的个数,一般情况下第一次预读是4个页 */
    ra->size = get_init_ra_size(req_size, max_pages);
    ra->async_size = ra->size > req_size ? ra->size - req_size : ra->size;

readit:
    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;
        }
    }
    /* 
     * 经过一点处理以后,会调用__do_page_cache_readahead函数,执行具体的从磁盘读取的流程 
     * 区别在于它是基于ra->start ra->async_size等信息进行读取
     * */
    return ra_submit(ra, mapping, filp);
}

当第一个页(page index=0)传入函数时,跳到initial_readahead部分,初始化ra->start、ra->size以及ra->async_size等信息,然后调用ra_submit进行读取。

当第一个页以外传入函数时,需要根据hit_readahead_marker判断同步预读还是异步预读,同步则根据offset和req_size进行预读,如果是异步则通过ra->start以及ra->async_size进行预读。

ondemand_readahead函数的核心是__do_page_cache_readahead函数,它会根据传入的参数,从磁盘读取特定范围的数据:

unsigned 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); // 将要读取的页存入到这个list当中
    int page_idx;
    unsigned int nr_pages = 0;
    loff_t isize = i_size_read(inode); // 得到文件的大小
    gfp_t gfp_mask = readahead_gfp_mask(mapping);

    end_index = ((isize - 1) >> PAGE_SHIFT); // 根据文件大小计算得到最后一个页的index

    for (page_idx = 0; page_idx < nr_to_read; page_idx++) {
        pgoff_t page_offset = offset + page_idx; // 计算得到page index

        if (page_offset > end_index) // 超过了文件的尺寸就break,停止读取
            break;

        rcu_read_lock();
         // 查看是否在page cache,如果已经在了cache中,再判断是否为脏,要不要进行读取
        page = radix_tree_lookup(&mapping->i_pages, page_offset);
        rcu_read_unlock();
        if (page && !radix_tree_exceptional_entry(page)) {
            if (nr_pages)
                read_pages(mapping, filp, &page_pool, nr_pages, gfp_mask);
            nr_pages = 0;
            continue;
        }
        // 如果不存在,则创建一个page cache结构
        page = __page_cache_alloc(gfp_mask);
        if (!page)
            break;
        // 设定page cache的index
        page->index = page_offset;
        // 加入到list当中
        list_add(&page->lru, &page_pool);
        // !!! 注意计算值,给这一个页加上Readahead的标志
        if (page_idx == nr_to_read - lookahead_size)
            SetPageReadahead(page);
        nr_pages++;
    }

    /*
     * 如果nr_pages大于0,则表示有页要进行读取
     * 执行read_pages从磁盘进行读取
     */
    if (nr_pages)
        read_pages(mapping, filp, &page_pool, nr_pages, gfp_mask);
out:
    return nr_pages;
}

Readahead机制的实例分析

下面通过一个顺序读的例子说明Linux预读机制的执行:

用户需要连续访问某个文件连续32个页(page index=0~31)的数据,那么它在预读机制下的访问行为是:

用户访问第一个页,page index=0,触发同步预读机制,一次性从磁盘读取4个页,即第1~4个页(page index=0~3)
用户访问第二个页,page index=1,含有特殊标志,触发异步预读机制,一次性从磁盘读取8个页,即第5~12个页(page index=4~11)
用户访问第三个页,page index=2,命中,直接返回给用户
用户访问第四个页,page index=3,命中,直接返回给用户
用户访问第五个页,page index=4,含有特殊标志,触发异步预读机制,一次性从磁盘读取16个页,即第13~28个页(page index=12~27)
用户访问第六个页,page index=5,命中,直接返回给用户
用户访问第七个页,page index=6,命中,直接返回给用户
用户访问第八个页,page index=7,命中,直接返回给用户
用户访问第九个页,page index=8,命中,直接返回给用户
用户访问第十个页,page index=9,命中,直接返回给用户
用户访问第十一个页,page index=10,命中,直接返回给用户
用户访问第十二个页,page index=11,命中,直接返回给用户
用户访问第十三个页,page index=12,含有特殊标志,触发异步预读机制,一次性从磁盘读取32个页,即第12~42个页(page index=13~43),但是由于page index=12~27的页上一次预读就将页读入了page cache,因此会跳过,实际上只会从磁盘读取page index=28~43页。
…以此类推

当访问第一个页时,跳到initial_readahead部分,初始化ra->start=0、ra->size=4以及ra->async_size=3,因此预读取了page index=0~3的页,并且给page index=1的页加上了标志。

访问第二个页(page index=1)时,由于有预读标志,因此进行异步预读。异步预读会增大预读页数,将预读页数由4个页增大到8个页,以次类推。
 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值