主要描述从用户态启动文件读开始,直到磁盘驱动,linux内核代码所走过的流程。阅读者需要对linux内核的内存管理、ext系列的文件系统,块设备,页高速缓存等有一定了解,不了解也没关系,顺着代码读可能会吃力点而已,鉴于不可能将代码全贴出来,中间缺失的部分,请大家自行脑补吧。至于写文章的原因,作为一名高效的客服,电话咨询来临时,就要把链接给出来,没有现成的就自己造了。

linux内核版本号:3.0.13-0.27 sles11sp2版本。

首先看系统调用处代码:

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)

然后系统通过vfs_read函数进入,通过vfs的文件对象操作函数read,进行操作。

if (file->f_op->read)
    ret = file->f_op->read(file, buf, count, pos);
else
    ret = do_sync_read(file, buf, count, pos);

其中read函数是根据不同的文件系统定义初始化不同的,其中ext3文件系统初始化如下:

const struct file_operations ext3_file_operations = {
 .llseek = generic_file_llseek,
 .read = do_sync_read,
 .write = do_sync_write,
 .aio_read = generic_file_aio_read,
 .aio_write = generic_file_aio_write,
 .unlocked_ioctl = ext3_ioctl,
#ifdef CONFIG_COMPAT
 .compat_ioctl = ext3_compat_ioctl,
#endif
 .mmap = generic_file_mmap,
 .open = dquot_file_open,
 .release = ext3_release_file,
 .fsync = ext3_sync_file,
 .splice_read = generic_file_splice_read,
 .splice_write = generic_file_splice_write,
};

read被定义为do_sync_read函数,在do_sync_read函数中初始化了iovec,kiocb结构体,将一些函数入参藏到里面,需要注意调用到里面会释放出来,注意要内外对应。此时系统进入了一个死循环,开始以一个页大小方式反复读数据,最后完成读任务。

for (;;) {
      ret = filp->f_op->aio_read(&kiocb, &iov, 1, kiocb.ki_pos);
      if (ret != -EIOCBRETRY)
           break;
      wait_on_retry_sync_kiocb(&kiocb);
}

系统调用aio_read函数,前面可知是generic_file_aio_read函数,然后进入到里面。需要注意的是,内核很多函数命名是不规范了,往往函数名不能表意,很多情况下按单词理解函数刚好反了。

generic_file_aio_read并不是异步读,函数中的一些检查操作请自动忽略,函数通过文件对象标准flag判断读文件是不是O_DIRECT形式,如果是,则进入直读模式,也就是跳过页缓存,实际代码实现上差别很大,O_DIRECT放到后面再写,先说其他的。函数则再次初始化了desc结构体,又藏了一些入参。

进入do_generic_file_read,内核通过read的偏移量ppos,获取的基树的索引index,这个时候就根据索引查找页高速缓存:

find_page:
    page = find_get_page(mapping, index);
    if (!page) {
        page_cache_sync_readahead(mapping,
              ra, filp,
              index, last_index - index);
        page = find_get_page(mapping, index);
        if (unlikely(page == NULL))
              goto no_cached_page;
        }
        if (PageReadahead(page)) {
              page_cache_async_readahead(mapping,
                    ra, filp, page,
                    index, last_index - index);
        }
        if (!PageUptodate(page)) {
              if (inode->i_blkbits == PAGE_CACHE_SHIFT ||
                       !mapping->a_ops->is_partially_uptodate)
                  goto page_not_up_to_date;
                  if (!trylock_page(page))
                  goto page_not_up_to_date;
                 /* Did it get truncated before we got the lock? */              if (!page->mapping)
              goto page_not_up_to_date_locked;
              if (!mapping->a_ops->is_partially_uptodate(page,
                     desc, offset))
              goto page_not_up_to_date_locked;
              unlock_page(page);
       }

读取的页有可能不在页高速缓存中,page_cache_sync_readahead函数看是否在预读中找到,(关于文件预读机制,后面单独写),如果没有找到,就进入no_cached_page里,为准备读取的页分配缓存,然后从文件系统上读取数据。

error = mapping->a_ops->readpage(filp, page);

页的磁盘读取是通过调用readpage函数完成的,由于ext3的日志处理有三种方式,所以初始化的函数也有3个,即ext3_ordered_aops、ext3_writeback_aops、ext3_journalled_aops。因为ext3默认的日志写入方式是ordered,所以后面都是以ordered为准。虽然读操作和日志没有关系。

static const struct address_space_operations ext3_ordered_aops = {
 .readpage = ext3_readpage,
 .readpages = ext3_readpages,
 .writepage = ext3_ordered_writepage,
 .write_begin = ext3_write_begin,
 .write_end = ext3_ordered_write_end,
 .bmap = ext3_bmap,
 .invalidatepage = ext3_invalidatepage,
 .releasepage = ext3_releasepage,
 .direct_IO = ext3_direct_IO,
 .migratepage = buffer_migrate_page,
 .is_partially_uptodate = block_is_partially_uptodate,
 .error_remove_page = generic_error_remove_page,
};

ext3_readpage中将ext3_get_block传入给mpage_readpage,先记下了。同时在do_mpage_readpage中将ext3_get_block作为get_block传递进去。

static struct bio *
do_mpage_readpage(struct bio *bio, struct page *page, unsigned nr_pages,
 sector_t *last_block_in_bio, struct buffer_head *map_bh,
 unsigned long *first_logical_block, get_block_t get_block)
{
   ..........
    while (page_block < blocks_per_page) {
        map_bh->b_state = 0;
        map_bh->b_size = 0;

        if (block_in_file < last_block) {
            map_bh->b_size = (last_block-block_in_file) << blkbits;
            /*下面直接调用ext3_get_block读取相应的物理块,其中buffhead中会
             将对应的物理块号存放到b_blocknr字段中*/            if (get_block(inode, block_in_file, map_bh, 0))
                goto confused;
            *first_logical_block = block_in_file;

    /*判断块是不是连续的,连续的则进行合并提交一个bio*/    if (page_block && blocks[page_block-1] != map_bh->b_blocknr-1)
     goto confused;
    nblocks = map_bh->b_size >> blkbits;
    for (relative_block = 0; ; relative_block++) {
       if (relative_block == nblocks) {
           clear_buffer_mapped(map_bh);
            break;
        } else if (page_block == blocks_per_page)
            break;
          /*最终将b_blocknr存放到了blocks数组中*/          blocks[page_block] = map_bh->b_blocknr+relative_block;
          page_block++;
           block_in_file++;
        }
     }

     /*bio开始为0,后续分配*/    if (bio && (*last_block_in_bio != blocks[0] - 1))
        bio = mpage_bio_submit(READ, bio);

alloc_new:
    if (bio == NULL) {
       /*将blocks开始的物理块号附加到bio*/        bio = mpage_alloc(bdev, blocks[0] << (blkbits - 9),
                min_t(int, nr_pages, bio_get_nr_vecs(bdev)),
                GFP_KERNEL);
        if (bio == NULL)
            goto confused;
    }
/*向设备层提交bio*/bio = mpage_bio_submit(READ, bio);

/*不连续的块则针对每个块分配一个bio进行提交*/confused:
    if (bio)
        bio = mpage_bio_submit(READ, bio);
    if (!PageUptodate(page))
            block_read_full_page(page, get_block);
    else
        unlock_page(page);
    goto out;
}

do_mpage_readpage大致就是通过传递进来的page结构体通过

block_in_file = (sector_t)page->index << (PAGE_CACHE_SHIFT - blkbits);

获取page对应文件的块相对位置,然后通过ext3_get_block获取磁盘的绝对块号,见块号赋值给BIO,然后提交给设备层读取,即完成了一次读文件的过程。


Linux内核读文件流程来自于OenHan

链接为:http://oenhan.com/linux-kernel-read

7 thoughts on “Linux内核读文件流程”

  1. 请问大神,上述代码中的 PageReadahead 函数是做什么的?

    找了半天也没有找到函数定义

  2. 请问大神 linux读裸盘的流程是不是和这个不一样呢?是不是把裸盘整个空间看成一个文件了。

      1. @OENHAN 就是一个普通的硬盘 不进行格式化 也不挂载。直接通过open(“/dev/sda”,O_RDWR)这样的方式来读写

发表回复