본문 바로가기

Software/Virtualization

VirtIO: Virtual I/O

VirtIO

기본이 된 논문은 RUSSELL, Rusty. virtio: towards a de-facto standard for virtual I/O devices. ACM SIGOPS Operating Systems Review, 2008, 42.5: 95-103.이다. 이 글은 VirtIO의 front-end driver 설명에 기반하여 작성되었다.

VirtIO

Virtual I/O의 약자로써, Guest OS와 Host간 paravirtualized I/O를 지원하기 위한 표준화1된 인터페이스이다.

1. What is VirtIO

VirtIO는 현재 Linux KVM에서 활발히 사용되고 있는 표준 인터페이스이다. 가상화 I/O 인터페이스에 대한 표준을 정의한다. 주요 이유는 가상화 드라이버 개발의 용이성과 재사용성이다.

2. VirtIO architecture

Driver abstractions with virtio2

KVM은 Xen과 유사하게 분리 드라이버 모델을 차용하고 있다. 즉, front-end drivers와 back-end drivers를 활용하여 paravirtualized I/O를 수행한다. 이를 위해서는 Guest OS가 paravirtualized I/O를 위한 front-end drivers를 가지고 있어야 한다. 이러한 분리 드라이버 모델 구조는 fully virtualized I/O에 비해 큰 성능이득을 가져온다. 그 이유는 device를 emulation할 필요 없이 back-end drivers로 I/O request를 바로 보낼 수 있기 때문이다. 


2-1. 드라이버의 동작 메커니즘

동작메커니즘은 CPU 전가상화와 반가상화에 따라 다르다.


2-1-1. CPU 전가상화 위 VirtIO

Front-end driver는 back-end driver와 통신해야 하는데 전가상화의 경우 명시적인 전달 경로가 없으므로 다음과 같은 경로를 거친다.

3

  1. Front-end driver에서 I/O request(+data)를 memory에 write
  2. Front-end driver에서 I/O instruction 발생 (privileged instruction)
  3. VT-x(또는 AMD-v) 기술에 의해 VM_EXIT trap 발생하여 hypervisor로 컨트롤 전이
  4. Hypervisor는 QEMU를 수행하고 있는 vCPU를 schedule
  5. QEMU의 back-end driver는 memory에 저장된 I/O request를 hypervisor의 실제 driver로 포워딩


2-1-2. CPU 반가상화 위 VirtIO

Front-end driver는 back-end driver와 직접 통신할 수 있다. (메모리 공유와 이벤트 채널을 이용)

  1. Front-end driver에서 I/O request(+data)를 memory에 write
  2. 이벤트 채널을 통해 hypervisor로 I/O request event 전송
  3. Hypervisor의 back-end driver는 I/O request event를 수신하여 I/O request(+data)를 memory에서 읽음
  4. 실제 driver로 I/O request(+data)를 forwarding

3. VirtIO Communication via VirtQueue in Transport Layer

Front-end driver에서 I/O 메커니즘을 추상화 하였는데 이런 추상화된 메커니즘을 Virtqueue라 지칭한다. VirtIO는 추상화된 I/O 메커니즘을 위하여 API set을 제공한다. (메커니즘에서 사용되는 자료구조는 virtio-ring이라 지칭한다.)

virtio layers4

Virtqueue의 API set는 다음과 같다.

struct virtqueue_ops {
 int (*add_buf)(struct virtqueue *vq,
                struct scatterlist sg[],
                unsigned int out_num,
                unsigned int in_num,
                void *data);
 void (*kick)(struct virtqueue *vq);
 void *(*get_buf)(struct virtqueue *vq, unsigned int *len);
 void (*disable_cb)(struct virtqueue *vq);
 bool (*enable_cb)(struct virtqueue *vq);
};

add_buf는 virtqueue에 buffer를 삽입한다.

하나의 buffer는 데이터를 struct scatterlist 형태로 제공한다. 일반적으로 데이터는 메모리에 연속적이지 않고 scattered 되어 있을 수 있다. 이 때문에 struct scatterlist를 이용한다. out_num은 데이터중 readable 데이터의 갯수, in_num은 writable 데이터의 갯수를 의미한다. (Total # of data = # of readable + # of writable). data는 buffer 자체의 토큰(identifier)을 의미한다.

kick은 buffer를 받는쪽에게 request가 pending이 되어있음을 알린다. 알리는 방법은 개발자에게 달려있다. 단, hypercall을 주로 쓴다.

get_buf는 처리 완료된 buffer에 접근하기 위하여 사용된다.

disable_cbenable_cb는 reuqest 완료시 발생하는 callback을 disable 또는 enable 하기 위해 사용한다. callback은 host의 request 처리 완료 interrupt에 의해 깨어난다.


3-1. About vring(virtio-ring)

Virtqueue에서 실제 전송되는 데이터는 vring의 형태를 갖는다. (즉, buffer는 여러개의 vring_desc로 나뉘어져 담긴다.) Vring은 세가지 종류로 이루어져 있다.

  • Descriptor table

    struct vring_desc
    {
      __u64 addr;
      __u32 len;
      __u16 flags;
      __u16 next;
    };
    
    • 여러개의 vring_desc로 이루어져 있다.
    • vring_desc는 데이터의 physical address(not machine address)와 data의 size인 length, 그리고 데이터가 readable/writable를 표현한 flag를 담고 있다. 또한 entry끼리의 chaining을 위한 next가 존재한다.
  • Available ring

    struct vring_avail
    {
      __u16 flags;
      __u16 idx;
      __u16 ring[NUM];
    };
    
    • 하나의 buffer는 여러개의 vring_desc로 이루어지고 이는 테이블에 기록되는데 그 중, 첫번째의 vring_desc index를 ring[NUM]에 저장한다.
    • Flag는 I/O 완료 후, request 완료 interrupt를 guest에 발생 여부를 설정한다.
  • Used ring
    struct vring_used_elem
    {
      __u32 id;
      __u32 len;
    };
    struct vring_used
    {
      __u32 flags;
      __u32 idx;
      struct vring_used_elem ring[];
    };
    
    • Host에서 생성된다.
    • vring_avail과 비슷하다. 단, 요청된 vring_desc들이 처리 되었음을 알려줄 때 사용한다.
    • Flag를 이용하여 guest가 kick을 날리지 않아도 괜찮은지 여부에 대하여 알려준다. (??)

4. Mechanism Step by Step (VirtIO Block Driver)

Block read request를 guest에서 발생시켰다고 가정하자. 그렇다면 guest의 front-end driver에서는 세가지 descriptor를 준비해야 한다.

  1. Read request를 담은 meta data를 포함한 vring_desc (read_only)
  2. 읽은 데이터를 저장할 빈 공간을 가리키는 vring_desc (write_only)
  3. I/O의 성공/실패 여부를 담을 vring_desc (write_only)

이를 descriptor table에 기록하고 첫번째 데이터(빈 공간을 가리키는 vring_desc)의 index를 vring_avail을 사용하여 available ring에 기록한다. 이 과정은 add_buf에서 일어난다. 이후, kick하여 pending request가 있음을 host에게 알린다.

Host는 read request를 처리하여 vring_used를 이용하여 이 사실을 guest에게 알린다. 만일 받았던 vring_avail에 인터럽트를 발생하라고 flag가 세팅되어있다면 interrupt를 발생하여 guest에게 알린다. Front-end driver의 callbackget_buf를 이용하여 처리된 데이터를 얻는다.


  1. 제일 큰 시장을 차지하고 있는 Xen hypervisor는 이러한 표준을 따르지 않고 Xen Bus를 이용한다. 

  2. https://www.ibm.com/developerworks/library/l-virtio/ 

  3. http://prog3.com/sbdm/blog/liukuan73/article/details/47049311 

  4. http://prog3.com/sbdm/blog/liukuan73/article/details/47049311