Virtio.png

개요

Virtio는 반가상화에서 디바이스를 구현하기 위한 방식으로 사용된다. 그러나 Qemu와 같은 대중적인 전가상화가상 머신에서도 Virtio와 같은 하드웨어 반가상화 기법을 지원한다. 이는 Virtio가 직접 커널을 수정하는 것이 아닌, 디바이스 드라이버를 이용하기 때문에 쉽게 Full Virtualizatio에서도 사용할 수 있기 때문이다. Virtio는 Hypervisor에 위치한 하드웨어와 직접 통신할 수 있는 특별한 Virtio-Driver를 GuestOS에 설치함으로써, 불필요한 Trap을 이용하여 Hardware emulation을 하는 Overhead를 줄임으로써 비약적으로 하드웨어 성능을 향상시킬 수 있다

작동 방식

기존의 Full Virtualization에서는 하드웨어가 GuestOS의 I/O request를 intercept하여서 진짜 하드웨어와 상호작용하고 결과를 Emulation해서 제공하여야 하였다. 이러한 방식은 flexiblity측면에서는 매우 효과적인 방식이지만, 매우 느리다는 단점이 있었다. 따라서 이러한 작업을 하는 것보다 Guest에 front-driver를 Hypervisor에 back-end driver를 설치하여 이 둘이 통신하도록 하여 하드웨어 interaction을 구현하는 방식이 고안되었고, 이 Abstraction이 바로 Virtio이다.

Virtio API는 간단한 ring buffer를 이용하여 command와 data를 host와 주고 받는 방식으로 구현된다. 이 ring buffer의 개수는 각 드라이버의 목적에 따라서 다르게 설정될 수 있다. 일례로, 통상적인 Virtio driver는 2개의 링버퍼를, Block device driver는 1개의 링버퍼를 사용하여 통신한다. 각각의 큐는 guest operating system에 의하여 설정되어야 한다. guest는 queue select register와 같은 virtio에서 제공하는 기능을 이용하여, 필요한 메모리의 양과 같은 기본적인 작업을 세팅하고, 이 모든 작업은 이후 host에 의해 전달되어 서로 통신할 수 있는 매개체로 쓰이게 된다.

Virtqueue

가상화된 디바이스에서 데이터 전송 메커니즘은 "virtqueue"로 불리우는데, 각 디바이스는 0개 이상의 virtqueue를 가질 수 있다. Virtqueues는 공유된 링 버퍼를 기반으로 하고, 이것이 virtqueues가 종종 Virtio 링 버퍼라고 불리는 이유다. 링 버퍼가 사용되는 이유는, 큐와 같은 전통적인 Producer consumer모델을 위한 자료구조와 달리, 링버퍼는 Lock-free data structure로 만들 수 있기 때문이다. 이는 점점 I/O가 빨라지는 환경에서 락 없이 사용하는 것이 매우 중요하기 때문이다.

드라이버는 대기 중인 버퍼를 큐에 추가하여 디바이스에서 요청을 사용 가능하게 만든다. 즉, virtqueue에 요청을 설명하는 버퍼를 추가하고 선택적으로 드라이버 이벤트를 트리거한다. 이는 디바이스에 사용 가능한 버퍼 알림을 보내는 것이다.

디바이스는 요청을 실행하고 완료되면 사용된 버퍼를 큐에 추가하여 드라이버가 사용한 버퍼로 표시한다. 디바이스는 디바이스 이벤트를 트리거하고, 사용된 버퍼 알림을 드라이버로 보낼 수 있다.

Virtio큐를 사용하는 방법은 Split virtqueue와 Packed virtqueue두가지가 존재하는데, 이 문서에서는 Spec 1.0에 존재하는 기본 작동 방식인 Split virtqueue만을 설명한다.

Split virtqueue

Split virtqueue는 최대 3개의 부분으로 구성될 수 있다.

  • Descriptor Area - 버퍼 설명에 사용됨
  • Driver Area - 드라이버에서 디바이스로 제공되는 데이터를 가진다. Avail virtqueue라고 불리기도 한다.
  • Device Area - 디바이스에서 드라이버로 제공되는 데이터를 가진다. Used virtqueue라고 불리기도 한다.

스플릿 virtqueue의 각 부분 (Area)는 메모리에서 물리적으로 연속적이어야 하며 정렬(Align)요구 사항이 있다. 각 버퍼의 주소는 드라이버의 시점에서 할당된다. 즉 각 버퍼의 주소값은 GPA를 가지고 있기 때문에, 호스트에서 저장된 주소를 변환해야 한다.

Descriptor Area

디스크립터 영역은 드라이버가 디바이스에 사용하는 버퍼를 나타낸다.

#[repr(C, align(16))]
#[derive(Debug)]
pub struct VirtqDesc {
    /// Address (guest-physical)
    pub addr: Physical,
    /// Length.
    pub len: u32,
    /// The flags.
    pub flags: VirtqDescFlags,
    /// Next field if flags & NEXT.
    next: u16,
}
  • addr: 게스트의 데이터를 가지고 있는 버퍼의 물리 주소를 가지고 있다.
  • len: 게스트의 데이터를 가지고 있는 버퍼의 길이를 가지고 있다.
  • flags: NEXT (버퍼가 다음 필드에 계속 되는지.) WRITE (버퍼가 디바이스 쓰기 전용인지, 아니면 읽기 전용인지), INDIRECT (대량의 요청을 동시에 배칭으로 처리하는 경우 사용됨.)으로 구성되어 있다.
  • next: Flags에 NEXT비트가 있을경우 Next로 가는 필드의 버퍼주소를 담고 있다.

Driver Area (Avail)

Virtio Driver area.png

드라이버 영역은 이용가능한 영역을 이용하여 버퍼를 디바이스에 넘긴다. 각각의 링 버퍼의 항목은 디스크립터 영역의 헤드를 가르킨다. 드라이버 영역은 드라이버 (게스트)에 의해서 쓰여지고 디바이스 (하이퍼바이저)에 의해서 읽힌다.

#[repr(C, align(2))]
pub struct VirtqAvail {
    flags: u16,
    idx: u16,
    ring: [u16; 0],
    _pin: core::marker::PhantomPinned,
}
  • flags: 드라이버가 버퍼를 사용할 때 따로 인터럽트를 받을 것인지 아니면 안 받을 것인지를 디바이스에 알리기 위해서 사용된다.
  • idx: 드라이버가 링에 다음 디스크립터 항목을 넣은 위치를 나타낸다. idx는 0부터 시작해서 큐 크기까지 증가하며, 다시 0으로 돌아간다. idx는 디스크립터 영역의 인덱스가 아니다. idx가 가르키는 항목은 Driver area의 링 배열의 인덱스 이며, 내부에 몇번 디스크립터 영역으로 가는지가 표시되어 있다.
  • ring: idx에의해서 가르켜지는 디스크립터 영역의 위치가 저장된다.

Device Area (Used)

Device 영역은 장치가 버퍼사용을 완료하면 어떻게 하였는지를 드라이버 (게스트)에게 알려주기 위해서 사용되는 곳이다. 이 영역은 Driver area와는 다르게 Device만이 쓰고 Driver가 읽는다.

#[repr(C)]
pub struct VirtqUsedElem {
    /// Index of start of used descriptor chain.
    id: u32,
    /// Total length of the descriptor chain which was used (written to)
    len: u32,
}
* id: 디스크립터 영역의 시작 인덱스를 나타낸다.
* len: 디스크립터 영역의 얼마만큼을 디바이스에서 작성했는지를 나타낸다.

#[repr(C, align(4))]
pub struct VirtqUsed {
    flags: u16,
    idx: u16,
    ring: [VirtqUsedElem; 0],
    // used_event
    _pin: core::marker::PhantomPinned,
}
  • flags: 드라이버가 버퍼를 사용할 때 따로 인터럽트를 받을 것인지 아니면 안 받을 것인지를 디바이스에 알리기 위해서 사용된다.
  • idx: ring의 인덱스를 나타낸다.
  • ring: 디바이스 영역에서 사용하는 디스크립터의 인덱스가 저장된다.
Virtio Device area.png

위의 경우에서는 Chain을 사용한 경우인데, Next필드를 통해서 다음으로 갈 디스크립터 영역이 지정되어 있다. 우선 디바이스가 Avail queue를 통해서 Descript area의 0번 그리고 1번이 사용될 예정이라고 마킹을 한다. 그러면 디바이스는 이 정보를 받아서, 적절한 처리를 한후 Used queue를 통해서 디스크립터의 몇번 영역의 어느정도의 길이만큼이 반환된 결과 값인지를 마킹한다. 이를 통해서 디바이스와 드라이버는 서로 정보를 주고 받을 수 있다.

vhost

vhost는 virtio의 디바이스 파트를 kernel에다가 구현하여서 virtio backend를 커널에서 돌릴 수 있도록 하는 기술이다. vhost는 유저 scheduler cost (User는 kernel보다 후순위로 처리됨으로 예측 하지 못한 Latency가 발생한다), world switch cost(게스트에서 커널로 가서 다시 Qemu user process로 가야 하여서 매 처리마다 switch가 일어난다. 또한 Qemu내부적으로 사용하는 write, read, poll과 같은 시스템콜 또한 추가적인 오버헤드를 가져온다)를 없앰으로서, 대략 랜덤 write의 경우 100%에 해당하는 성능 향상을 보일 수 있었다. 이는 code path가 단순하고, 불필요한 world switch를 일으키는 system call들이 사라지고, qemu mutex lock과 같은 synchornization문제가 없기 때문에 나는 성능 향상이다.

vhost-net, vhost-blk처럼 자주 사용되는 디바이스들이 vhost를 support하고 있다.

More faster!

Virtio를 더욱 가속화 시키기 위해서 Vmware의 VirtFS와 같은 기법들이 새롭게 개발되고 연구되고 있다. 복잡한 I/O스택을 어떻게 하면 최적화 시킬 수 있을지가 항상 Virtio더 나아가 I/O Virtualization의 연구 화두로 존재해왔다.

참고

  1. https://wiki.osdev.org/Virtio
  2. https://developer.ibm.com/articles/l-virtio/
  3. https://docs.oasis-open.org/virtio/virtio/v1.1/csprd01/virtio-v1.1-csprd01.html
  4. https://www.redhat.com/en/blog/virtqueues-and-virtio-ring-how-data-travels
  5. http://www.virtualopensystems.com/en/solutions/guides/snabbswitch-qemu/