zoukankan      html  css  js  c++  java
  • KVM虚拟机IO处理过程(一) ----Guest VM I/O 处理过程

     虚拟化技术主要包含三部分内容:CPU虚拟化,内存虚拟化,设备虚拟化.本系列文章主要描述磁盘设备的虚拟化过程,包含了一个读操作的I/O请求如何从Guest Vm到其最终被处理的整个过程.本系列文章中引用到的linux内核代码版本为3.7.10,使用的虚拟化平台是KVM,qemu的版本是1.6.1.

        用户程序想要访问IO设备需要调用操作系统提供的接口,即系统调用.当在用户程序中调用一个read操作时,系统先保存好read操作的参数,然后调用int 80命令(也可能是sysenter)进入内核空间,在内核空间中,读操作的逻辑由sys_read函数实现.

        在讲sys_read的实现过程之前,我们先来看看read操作在内核空间需要经历的层次结构.从图中可以看出,read操作首先经过虚拟文件系统曾(vfs), 接下来是具体的文件系统层,Page cache层,通用块层(generic block layer),I/O调度层(I/O scheduler layer),块设备驱动层(block device driver layer),最后是块物理设备层(block device layer).

    • 虚拟文件系统层:该层屏蔽了下层的具体操作,为上层提供统一的接口,如vfs_read,vfs_write等.vfs_read,vfs_write通过调用下层具体文件系统的接口来实现相应的功能.
    • 具体文件系统层:该层针对每一类文件系统都有相应的操作和实现了,包含了具体文件系统的处理逻辑.
    • page cache层:该层缓存了从块设备中获取的数据.引入该层的目的是避免频繁的块设备访问,如果在page cache中已经缓存了I/O请求的数据,则可以将数据直接返回,无需访问块设备.
    • 通过块层:接收上层的I/O请求,并最终发出I/O请求.该层向上层屏蔽了下层设备的特性.
    • I/O调度层:   接收通用块层发出的 IO 请求,缓存请求并试图合并相邻的请求(如果这两个请求的数据在磁盘上是相邻的)。并根据设置好的调度算法,回调驱动层提供的请求处理函数,以处理具体的 IO 请求
    • 块设备驱动层:从上层取出请求,并根据参数,操作具体的设备.
    • 块设备层:真正的物理设备.
     
        了解了内核层次的结构,让我们来看一下read操作的代码实现.
         sys_read函数声明在include/linux/syscalls.h文件中,
    [cpp] view plaincopy
     
    1. asmlinkage long sys_read(unsigned int fd, char __user *buf, size_t count);  
         
          其函数实现在fs/read_write.c文件中:
    [cpp] view plaincopy
     
    1. SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)  
    2. {  
    3.     struct fd f = fdget(fd);  
    4.     ssize_t ret = -EBADF;  
    5.   
    6.     if (f.file) {  
    7.         loff_t pos = file_pos_read(f.file);  
    8.         ret = vfs_read(f.file, buf, count, &pos); //调用vfs layer中的read操作  
    9.         file_pos_write(f.file, pos);//设置当前文件的位置  
    10.         fdput(f);  
    11.     }  
    12.     return ret;  
    13. }  
     
        vfs_read函数属于vfs layer,定义在fs/read_write.c, 其主要功能是调用具体文件系统中对应的read操作,如果具体文件系统没有提供read操作,则使用默认的do_sync_read函数.
    [cpp] view plaincopy
     
    1. ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)  
    2. {  
    3.     ssize_t ret;  
    4.   
    5.     if (!(file->f_mode & FMODE_READ))  
    6.         return -EBADF;  
    7.     if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))  
    8.         return -EINVAL;  
    9.     if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))  
    10.         return -EFAULT;  
    11.   
    12.     ret = rw_verify_area(READ, file, pos, count);  
    13.     if (ret >= 0) {  
    14.         count = ret;  
    15.         if (file->f_op->read) {  
    16.             ret = file->f_op->read(file, buf, count, pos); //该函数由具体的文件系统指定  
    17.         } else  
    18.             ret = do_sync_read(file, buf, count, pos);  //内核默认的读文件操作  
    19.         if (ret > 0) {  
    20.             fsnotify_access(file);  
    21.             add_rchar(current, ret);  
    22.         }  
    23.         inc_syscr(current);  
    24.     }  
    25.   
    26.     return ret;  
    27. }  
        file->f_op的类型为struct file_operations, 该类型定义了一系列涉及文件操作的函数指针,针对不同的文件系统,这些函数指针指向不同的实现.以ext4 文件系统为例子,该数据结构的初始化在fs/ext4/file.c,从该初始化可以知道,ext4的read操作调用了内核自带的do_sync_read()函数
    [cpp] view plaincopy
     
    1. const struct file_operations ext4_file_operations = {  
    2.     .llseek     = ext4_llseek,  
    3.     .read       = do_sync_read,  
    4.     .write      = do_sync_write,  
    5.     .aio_read   = generic_file_aio_read,  
    6.     .aio_write  = ext4_file_write,  
    7.     .unlocked_ioctl = ext4_ioctl,  
    8. #ifdef CONFIG_COMPAT  
    9.     .compat_ioctl   = ext4_compat_ioctl,  
    10. #endif  
    11.     .mmap       = ext4_file_mmap,  
    12.     .open       = ext4_file_open,  
    13.     .release    = ext4_release_file,  
    14.     .fsync      = ext4_sync_file,  
    15.     .splice_read    = generic_file_splice_read,  
    16.     .splice_write   = generic_file_splice_write,  
    17.     .fallocate  = ext4_fallocate,  
    18. };  
        do_sync_read()函数定义fs/read_write.c中,
    [cpp] view plaincopy
     
    1. ssize_t do_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)  
    2. {  
    3.     struct iovec iov = { .iov_base = buf, .iov_len = len };  
    4.     struct kiocb kiocb;  
    5.     ssize_t ret;  
    6.   
    7.     init_sync_kiocb(&kiocb, filp);//初始化kiocp,描述符kiocb是用来记录I/O操作的完成状态  
    8.     kiocb.ki_pos = *ppos;  
    9.     kiocb.ki_left = len;  
    10.     kiocb.ki_nbytes = len;  
    11.   
    12.     for (;;) {  
    13.         ret = filp->f_op->aio_read(&kiocb, &iov, 1, kiocb.ki_pos);//调用真正做读操作的函数,ext4文件系统在fs/ext4/file.c中配置  
    14.         if (ret != -EIOCBRETRY)  
    15.             break;  
    16.         wait_on_retry_sync_kiocb(&kiocb);  
    17.     }  
    18.   
    19.     if (-EIOCBQUEUED == ret)  
    20.         ret = wait_on_sync_kiocb(&kiocb);  
    21.     *ppos = kiocb.ki_pos;  
    22.     return ret;  
    23. }  
        在ext4文件系统中filp->f_op->aio_read函数指针只想generic_file_aio_read, 该函数定义于mm/filemap.c文件中,该函数有两个执行路径,如果是以O_DIRECT方式打开文件,则读操作跳过page cache直接去读取磁盘,否则调用do_generic_sync_read函数尝试从page cache中获取所需的数据.
    [cpp] view plaincopy
     
    1. ssize_t  
    2. generic_file_aio_read(struct kiocb *iocb, const struct iovec *iov,  
    3.         unsigned long nr_segs, loff_t pos)  
    4. {  
    5.     struct file *filp = iocb->ki_filp;  
    6.     ssize_t retval;  
    7.     unsigned long seg = 0;  
    8.     size_t count;  
    9.     loff_t *ppos = &iocb->ki_pos;  
    10.   
    11.     count = 0;  
    12.     retval = generic_segment_checks(iov, &nr_segs, &count, VERIFY_WRITE);  
    13.     if (retval)  
    14.         return retval;  
    15.   
    16.     /* coalesce the iovecs and go direct-to-BIO for O_DIRECT */  
    17.     if (filp->f_flags & O_DIRECT) {  
    18.         loff_t size;  
    19.         struct address_space *mapping;  
    20.         struct inode *inode;  
    21.   
    22.         struct timex txc;  
    23.         do_gettimeofday(&(txc.time));  
    24.   
    25.         mapping = filp->f_mapping;  
    26.         inode = mapping->host;  
    27.         if (!count)  
    28.             goto out; /* skip atime */  
    29.         size = i_size_read(inode);  
    30.         if (pos < size) {  
    31.             retval = filemap_write_and_wait_range(mapping, pos,  
    32.                     pos + iov_length(iov, nr_segs) - 1);  
    33.             if (!retval) {  
    34.                 retval = mapping->a_ops->direct_IO(READ, iocb,  
    35.                             iov, pos, nr_segs);  
    36.             }  
    37.             if (retval > 0) {  
    38.                 *ppos = pos + retval;  
    39.                 count -= retval;  
    40.             }  
    41.   
    42.             /* 
    43.              * Btrfs can have a short DIO read if we encounter 
    44.              * compressed extents, so if there was an error, or if 
    45.              * we've already read everything we wanted to, or if 
    46.              * there was a short read because we hit EOF, go ahead 
    47.              * and return.  Otherwise fallthrough to buffered io for 
    48.              * the rest of the read. 
    49.              */  
    50.             if (retval < 0 || !count || *ppos >= size) {  
    51.                 file_accessed(filp);  
    52.                 goto out;  
    53.             }  
    54.         }  
    55.     }  
    56.   
    57.     count = retval;  
    58.     for (seg = 0; seg < nr_segs; seg++) {  
    59.         read_descriptor_t desc;  
    60.         loff_t offset = 0;  
    61.   
    62.         /* 
    63.          * If we did a short DIO read we need to skip the section of the 
    64.          * iov that we've already read data into. 
    65.          */  
    66.         if (count) {  
    67.             if (count > iov[seg].iov_len) {  
    68.                 count -= iov[seg].iov_len;  
    69.                 continue;  
    70.             }  
    71.             offset = count;  
    72.             count = 0;  
    73.         }  
    74.   
    75.         desc.written = 0;  
    76.         desc.arg.buf = iov[seg].iov_base + offset;  
    77.         desc.count = iov[seg].iov_len - offset;  
    78.         if (desc.count == 0)  
    79.             continue;  
    80.         desc.error = 0;  
    81.         do_generic_file_read(filp, ppos, &desc, file_read_actor);  
    82.         retval += desc.written;  
    83.         if (desc.error) {  
    84.             retval = retval ?: desc.error;  
    85.             break;  
    86.         }  
    87.         if (desc.count > 0)  
    88.             break;  
    89.     }  
    90. out:  
    91.     return retval;  
    92. }  
        do_generic_file_read定义在mm/filemap.c文件中,该函数调用page cache层中相关的函数.如果所需数据存在与page cache中,并且数据不是dirty的,则从page cache中直接获取数据返回.如果数据在page cache中不存在,或者数据是dirty的,则page cache会引发读磁盘的操作.该函数的读磁盘并不是简单的只读取所需数据的所在的block,而是会有一定的预读机制来提高cache的命中率,减少磁盘访问的次数. 
     
        page cache层中真正读磁盘的操作为readpage系列,readpage系列函数具体指向的函数实现在fs/ext4/inode.c文件中定义,该文件中有很多个struct address_space_operation对象来对应与不同日志机制,我们选择linux默认的ordered模式的日志机制来描述I/O的整个流程, ordered模式对应的readpage系列函数如下所示.
    [cpp] view plaincopy
     
    1. static const struct address_space_operations ext4_ordered_aops = {  
    2.     .readpage       = ext4_readpage,  
    3.     .readpages      = ext4_readpages,  
    4.     .writepage      = ext4_writepage,  
    5.     .write_begin        = ext4_write_begin,  
    6.     .write_end      = ext4_ordered_write_end,  
    7.     .bmap           = ext4_bmap,  
    8.     .invalidatepage     = ext4_invalidatepage,  
    9.     .releasepage        = ext4_releasepage,  
    10.     .direct_IO      = ext4_direct_IO,  
    11.     .migratepage        = buffer_migrate_page,  
    12.     .is_partially_uptodate  = block_is_partially_uptodate,  
    13.     .error_remove_page  = generic_error_remove_page,  
    14. };  
        为简化流程,我们选取最简单的ext4_readpage函数来说明,该函数实现位于fs/ext4/inode.c中,函数很简单,只是调用了mpage_readpage函数.mpage_readpage位于fs/mpage.c文件中,该函数生成一个IO请求,并提交给Generic block layer.
    [cpp] view plaincopy
     
    1. int mpage_readpage(struct page *page, get_block_t get_block)  
    2. {  
    3.     struct bio *bio = NULL;  
    4.     sector_t last_block_in_bio = 0;  
    5.     struct buffer_head map_bh;  
    6.     unsigned long first_logical_block = 0;  
    7.   
    8.     map_bh.b_state = 0;  
    9.     map_bh.b_size = 0;  
    10.     bio = do_mpage_readpage(bio, page, 1, &last_block_in_bio,  
    11.             &map_bh, &first_logical_block, get_block);  
    12.     if (bio)  
    13.         mpage_bio_submit(READ, bio);  
    14.     return 0;  
    15. }  
     
        Generic block layer会将该请求分发到具体设备的IO队列中,由I/O Scheduler去调用具体的driver接口获取所需的数据.
     
        至此,在Guest vm中整个I/O的流程已经介绍完了,后续的文章会介绍I/O操作如何从Guest vm跳转到kvm及如何在qemu中模拟I/O设备.
     
     
    参考资料:
    转载:http://blog.csdn.net/dashulu/article/details/16820281
  • 相关阅读:
    「SAP技术」SAP MM MB5M报表不显示特殊库存数据
    python-day3-条件判断与循环
    python-day2-运算符
    Mysql数据库意外崩溃导致表数据文件损坏无法启动的问题解决
    图书管理系统(Servlet+Jsp+Java+Mysql,附下载演示地址)
    求解最长递增子序列(LIS) | 动态规划(DP)+ 二分法
    HTML+CSS+JavaScript实现植物大战僵尸(附演示地址)
    面试官:如何在Integer类型的ArrayList中同时添加String、Character、Boolean等类型的数据? | Java反射高级应用
    面试官:手撕十大排序算法,你会几种?
    用x种方式求第n项斐波那契数,99%的人只会第一种
  • 原文地址:https://www.cnblogs.com/wuchanming/p/4689701.html
Copyright © 2011-2022 走看看