기본이 된 논문은 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
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와 통신해야 하는데 전가상화의 경우 명시적인 전달 경로가 없으므로 다음과 같은 경로를 거친다.
- Front-end driver에서 I/O request(+data)를 memory에 write
- Front-end driver에서 I/O instruction 발생 (privileged instruction)
- VT-x(또는 AMD-v) 기술에 의해 VM_EXIT trap 발생하여 hypervisor로 컨트롤 전이
- Hypervisor는 QEMU를 수행하고 있는 vCPU를 schedule
- QEMU의 back-end driver는 memory에 저장된 I/O request를 hypervisor의 실제 driver로 포워딩
2-1-2. CPU 반가상화 위 VirtIO
Front-end driver는 back-end driver와 직접 통신할 수 있다. (메모리 공유와 이벤트 채널을 이용)
- Front-end driver에서 I/O request(+data)를 memory에 write
- 이벤트 채널을 통해 hypervisor로 I/O request event 전송
- Hypervisor의 back-end driver는 I/O request event를 수신하여 I/O request(+data)를 memory에서 읽음
- 실제 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이라 지칭한다.)
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_cb
와 enable_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에 발생 여부를 설정한다.
- 하나의 buffer는 여러개의
-
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를 준비해야 한다.
- Read request를 담은 meta data를 포함한
vring_desc
(read_only) - 읽은 데이터를 저장할 빈 공간을 가리키는
vring_desc
(write_only) - 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의 callback
은 get_buf
를 이용하여 처리된 데이터를 얻는다.