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 plain copy
 
  1. asmlinkage long sys_read(unsigned int fd, char __user *buf, size_t count);  
     
      其函數實現在fs/read_write.c文件中:
[cpp]  view plain copy
 
  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 plain copy
 
  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 plain copy
 
  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 plain copy
 
  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 plain copy
 
  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 plain copy
 
  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 plain copy
 
  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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM