QEMU에서 블록 디바이스(virtio-blk)가 가상화되는 과정
디스크 장치 가상화 방식(전/반/하드웨어 보조)과 virtio 원리,
QEMU 초기화 단계의 virtio-blk 클래스 생성·인스턴스화·realize,
QEMU 시작 과정에서 `-drive` 옵션이 실제 디스크 이미지 파일을 여는 데까지(drive_new → blockdev_init → bdrv_open → qcow2/raw)를 추적
1) 디스크 장치 가상화 방식과 virtio 원리
2) QEMU 초기화 단계: virtio-blk 클래스 생성과 realize
3) QEMU 시작 과정: -drive 파싱과 drive_new
4) 디스크 이미지 열기 (bdrv_open → qcow2/raw)
1) 디스크 장치 가상화 방식과 virtio 원리
디스크 장치 가상화 방식

(1) 전가상화(Full Virtualization)
전가상화는 완전 가상화라고도 부르는데, 호스트 운영체제에 가상화 소프트웨어 계층(즉 가상머신 모니터 VMM 또는 Hypervisor)을 설치하여, 물리 하드웨어 자원을 완전히 가상화해서 여러 개의 독립된 가상머신(VM)으로 만든다. 각 가상머신은 모두 독립적으로 자신의 운영체제와 응용 프로그램을 실행할 수 있고, 운영체제를 수정할 필요가 없다.
전가상화 방식에서는 가상머신(모니터) 안의 운영체제가 자신이 가상머신 안에서 실행되고 있다는 사실을 인식하지 못한다.
특징과 장점
- 호환성 강함: 수정되지 않은 운영체제를 실행할 수 있고, 다양한 유형의 소프트웨어와 호환된다.
- 격리성 좋음: 가상머신 간에 서로 격리되어 보안성과 안정성을 보장한다.
- 유연성 높음: 여러 다른 운영체제를 실행해야 하는 환경에 적합하다.
적용 시나리오
- 기업 데이터 센터
- 클라우드 서비스 제공업체
- 높은 격리성과 보안성이 필요한 응용 시나리오
단점
전가상화 방식의 가장 큰 문제(단점)는, QEMU가 시뮬레이션하는 장치가 "번역관" 역할을 맡아서, 모든 명령어가 번역되어야 하므로 속도가 느리고 효율이 낮다는 점이다.
(2) 반가상화(Paravirtualization)
반가상화는 가상머신과 호스트 운영체제가 협력하여 함께 가상화 과정을 완성해야 한다. 이는 보통 가상머신의 운영체제를 수정해서 가상화 소프트웨어 계층과 직접 통신할 수 있도록 해야 한다.
반가상화 방식에서는 가상머신(모니터) 안의 운영체제가 자신이 가상머신 안에서 실행되고 있다는 사실을 안다.
특징과 장점
- 성능 우수: 가상화 계층의 부담을 줄여서 성능을 향상시킨다.
- 타깃성 강함: 특정 운영체제와 환경에 적합하다. 예를 들면 전용 서버 환경이다.
적용 시나리오
- 고성능과 저지연이 필요한 응용 시나리오
- 호환성 요구가 높지 않은 특정 운영체제 환경
단점
반가상화의 단점은 가상화가 충분히 완전하지 않고 철저하지 않다는 점이다.
(3) 하드웨어 보조 가상화(Hardware-Assisted Virtualization)
하드웨어 보조 가상화는 현대 프로세서의 하드웨어 특성을 이용해서 가상머신의 실행과 관리를 최적화하는 방식이다.
프로세서에는 가상머신 관리 프로그램(Hypervisor)을 지원하기 위한 특정 명령어 집합이 통합되어 있어서, 가상화 부담을 대폭 줄였다.
특징과 장점
- 성능이 네이티브에 근접: 하드웨어 지원 덕분에 가상머신의 성능이 베어메탈에 가깝다.
- 보안성 높음: 하드웨어 특성이 가상머신의 보안성을 강화한다.
- 배포 용이: 운영체제를 수정할 필요가 없어서 배포와 유지보수의 난이도가 간소화된다.
적용 시나리오
- 클라우드 컴퓨팅
- 빅데이터 처리
- 서버 통합
- 고성능과 고보안성이 필요한 응용 시나리오

성능 비교
- 전가상화: 모든 하드웨어 기능을 시뮬레이션해야 하므로 성능 부담이 상대적으로 크다.
- 반가상화: 가상화 계층의 부담을 줄여서 성능이 전가상화보다 우수하다.
- 하드웨어 보조 가상화: 하드웨어 지원 덕분에 성능이 네이티브 하드웨어에 근접한다.
호환성 비교
- 전가상화: 호환성이 강해서 수정되지 않은 운영체제를 실행할 수 있다.
- 반가상화: 운영체제의 지원과 수정이 필요하므로 호환성이 비교적 떨어진다.
- 하드웨어 보조 가상화: 운영체제를 수정할 필요가 없어서 호환성이 좋다.
virtio의 기본 원리
그렇다면 디스크 즉 블록 장치에 대해 어떤 가상화 방식을 선택하는 게 좋을까?
비교적 통용되는 방식은 반가상화를 선택하는 것이다.
즉 가상머신(모니터) 안의 운영체제가 자신이 가상머신 안에서 실행되고 있다는 사실을 알고, 가상머신은 호스트 운영체제와 협력하여 함께 가상화 과정을 완성해야 한다.
그러면 또 문제가 생긴다. 이미 가상머신의 운영체제를 수정해서 호스트 시스템과 협력할 수 있도록 하는데, 그렇다면 구체적인 구현 세부 사항은 어떠해야 할까?
각 장치, 심지어 각 제조사의 장치마다 따로 (반)가상화를 진행해야 할까, 아니면 통일된 방식으로 (반)가상화를 진행해야 할까?
이것이 바로 이번 회에서 중점적으로 설명하려는 내용 —— virtio의 기본 원리이다.
가상화 기술의 초기에는 서로 다른 가상화 기술이 서로 다른 디스크 장치(및 네트워크 장치 등)에 대해 서로 다른 드라이버를 구현했다.
그에 상응하여, 가상머신 안의 운영체제도 서로 다른 가상화 기술, 서로 다른 물리 저장 장비에 따라 서로 다른 드라이버를 로드해야 했다.
이렇게 되니 문제가 생겼다.
디스크 장치(및 네트워크 장치 등)가 너무 많아서, 드라이버가 어지럽게 많아졌다. 이러한 "각자 싸우는" 상황은 한편으로는 유지보수에 불리하고, 다른 한편으로는 코드가 마구잡이로 자라는 추세를 보이게 만들었다.
이는 가전 산업, 자동차 산업에서의 부품과 비슷한 점이 있다. 형태와 크기가 천차만별이라, 후기 조립과 수리, 유지보수 때 골치 아프게 만든다.
나중에는 이렇게는 안 된다고 생각해서, 가전 조립과 수리, 자동차 산업의 경험(추측)을 빌려서 통일된 표준을 만들어 표준화 과정을 완성하기로 했다.
그래서 virtio가 탄생했다. virtio의 전체 명칭은 Virtual IO로, 가상화 I/O라는 뜻이다. 이전에 언급한 것처럼, 디스크와 네트워크 등 장치는 모두 외부 장치에 속하고, 여기서는 IO의 범주에 속한다.

virtio는 가상머신에 통일된 인터페이스를 제공하는 역할을 한다. 즉, 가상머신 안의 운영체제가 로드하는 드라이버가 이전에는 천차만별이었는데, 이후에는 모두 통일해서 virtio를 로드하게 되었다.
virtio의 기본 아키텍처

이에 대응하여, 호스트에서는 서로 다른 virtio 백엔드를 구현해서 서로 다른 물리 장치에 맞출 수 있다.
virtio는 일종의 프론트엔드-백엔드 아키텍처로, 프론트엔드 드라이버(Front-End Driver)와 백엔드 장치(Back-End Device) 그리고 자체 정의된 전송 프로토콜을 포함한다.
전송 프로토콜을 통해서, virtio는 QEMU/KVM 방식에 사용될 수 있을 뿐만 아니라 다른 가상화 방식에도 사용될 수 있다.
예를 들어 가상머신은 꼭 QEMU일 필요는 없고, 다른 유형의 가상머신일 수도 있다.
백엔드도 꼭 QEMU에서 구현될 필요는 없고, 커널에서도 구현될 수 있다(이것이 실제로 vhost 방식이다).
virtio 아키텍처 계층화
virtio는 3개 층으로 나눌 수 있다.
(1) 프론트엔드 드라이버(Front-end drivers)
프론트엔드 드라이버는 즉 가상머신 내부의 virtio가 시뮬레이션하는 장치에 대응되는 드라이버로, 각 프론트엔드 장치마다 모두 대응되는 드라이버가 있어야 정상적으로 실행될 수 있다.
프론트엔드 드라이버의 주요 역할은 다음과 같다.
- 사용자 모드의 요청을 수신한다.
- 그 다음 전송 프로토콜에 따라 이 요청들을 캡슐화한다.
- 다시 I/O 포트를 읽고 쓴다.
- 알림을 보낸다. QEMU의 백엔드 장치로.
(2) 백엔드 장치/백엔드 드라이버(Back-end devices/drivers)
백엔드 장치는 QEMU 안에 있고, 프론트엔드 드라이버가 보낸 I/O 요청을 수신하고, 수신된 데이터에서 전송 프로토콜의 형식에 따라 분석을 진행하는 데 사용된다.
디스크 등 물리 장치와 상호작용해야 하는 요청에 대해서, 백엔드 장치는 물리 장치를 조작하여 요청을 완성하고, 인터럽트 메커니즘을 통해 프론트엔드 드라이버에게 통지한다.
virtio 큐(virtio queue, virtqueue)
virtio 프론트엔드와 백엔드 드라이버의 데이터 전송은 virtio 큐(virtio queue, virtqueue)를 통해 완성되는데, 한 장치는 여러 개의 virtio 큐를 등록하고, 각 큐는 서로 다른 데이터 전송을 처리하는 책임을 진다.
이 큐들은 제어 측면의 큐도 있고, 데이터 측면의 큐도 있다.
virtqueue 내부 구조

virtqueue는 vring을 통해 구현되고, vrtqueue와 vring을 두 층으로 나누는 분층 방법도 있어서, 아래와 같이 전체적으로 virtio를 4층으로 나눈다

vring은 가상머신과 QEMU 간에 공유되는 하나의 링 버퍼이다.
가상머신이 QEMU에 요청을 보내야 할 때 데이터를 미리 준비해두고, 데이터를 vring에 기술해 넣고, I/O 포트 하나에 쓴다.
그러면 QEMU는 vring에서 데이터 정보를 읽어낼 수 있고, 더 나아가 메모리에서 데이터를 읽어낸다.
QEMU가 요청을 완성한 후에도, 데이터 구조를 vring에 넣어두면, 프론트엔드 드라이버도 vring에서 데이터를 얻을 수 있다.

2) QEMU 초기화 단계: virtio-blk 클래스 생성과 realize
QEMU 초기화 단계의 블록 장치 가상화
이전 글을 봤다면 QEMU에서 C 언어를 사용하여 객체 지향 사상을 구현한 메커니즘에 대해 낯설지 않을 것이다. 디스크 즉 블록 장치에 대해서, 메커니즘도 같다.
static const TypeInfo virtio_blk_info = {
.name = TYPE_VIRTIO_BLK,
.parent = TYPE_VIRTIO_DEVICE,
.instance_size = sizeof(VirtIOBlock),
.instance_init = virtio_blk_instance_init,
.class_init = virtio_blk_class_init,
};
static void virtio_register_types(void)
{
type_register_static(&virtio_blk_info);
}
type_init(virtio_register_types)
virtio block device라는 클래스의 정의는 다층의 상속 관계가 있다. TYPE_VIRTIO_BLK의 부모 클래스는 TYPE_VIRTIO_DEVICE이다.
그러면 TYPE_VIRTIO_DEVICE의 부모 클래스는 또 무엇일까? hw/virtio/virtio.c 안에 있다.
static const TypeInfo virtio_device_info = {
.name = TYPE_VIRTIO_DEVICE,
.parent = TYPE_DEVICE,
.instance_size = sizeof(VirtIODevice),
.class_init = virtio_device_class_init,
.instance_finalize = virtio_device_instance_finalize,
.abstract = true,
.class_size = sizeof(VirtioDeviceClass),
};
static void virtio_register_types(void)
{
type_register_static(&virtio_device_info);
}
type_init(virtio_register_types)
보다시피, TYPE_VIRTIO_DEVICE의 부모 클래스는 TYPE_DEVICE이다. 그리고 TYPE_DEVICE의 부모 클래스는 TYPE_OBJECT이다. hw/core/qdev.c의 코드를 참조하자.
static const TypeInfo device_type_info = {
.name = TYPE_DEVICE,
.parent = TYPE_OBJECT,
.instance_size = sizeof(DeviceState),
.instance_init = device_initfn,
.instance_post_init = device_post_init,
.instance_finalize = device_finalize,
.class_base_init = device_class_base_init,
.class_init = device_class_init,
.abstract = true,
.class_size = sizeof(DeviceClass),
.interfaces = (InterfaceInfo[]) {
{ TYPE_VMSTATE_IF },
{ TYPE_RESETTABLE_INTERFACE },
}
};
static void qdev_register_types(void)
{
type_register_static(&device_type_info);
}
type_init(qdev_register_types)
TYPE_OBJECT까지 가면 끝이다. 이것은 "최상위 루트"이고, 부모 클래스가 없으며, 모든 클래스는 그것의 자손이다. hw/core/object.c의 코드를 참조하자.
static void register_types(void)
{
static TypeInfo interface_info = {
.name = TYPE_INTERFACE,
.class_size = sizeof(InterfaceClass),
.abstract = true,
};
static const TypeInfo object_info = {
.name = TYPE_OBJECT,
.instance_size = sizeof(Object),
.class_init = object_class_init,
.abstract = true,
};
type_interface = type_register_internal(&interface_info);
type_register_internal(&object_info);
}
type_init(register_types)
virtio block device로 돌아가서, 상속 관계는 다음과 같다
TYPE_OBJECT
---> TYPE_DEVICE
---> TYPE_VIRTIO_DEVICE
---> TYPE_VIRTIO_BLK

type_init 함수는 클래스 등록에 사용된다. 각 층마다 class_init이 있고, TypeImpl에서 xxxClass를 생성하는 데 사용된다. TYPE_OBJECT의 class_init은 object_class_init이고,
TYPE_DEVICE의 class_init은 device_class_init이며,
TYPE_VIRTIO_DEVICE의 class_init은 virtio_device_class_init이고,
TYPE_VIRTIO_BLK의 class_init은 virtio_blk_class_init이다.
각 계층은 또한 instance_init을 가질 수 있다(없을 수도 있다). xxxClass를 인스턴스로 초기화하는 데 사용된다.
TYPE_DEVICE의 instance_init은 device_initfn이고, TYPE_VIRTIO_BLK의 instance_init은 virtio_blk_instance_init이다.
각 계층마다 type_init 함수가 하나 있고, type_init 함수는 클래스 등록에 사용된다.
동시에 각 계층마다 class_init도 하나 있고, TypeImpl에서 xxxClass를 생성하는 데 사용된다.
이것은 객체 지향(C++과 Java같은)에서의 명백한 사상이 C 언어에서 이식된 표현이다.
TypeImpl에서 xxxClass 생성 - 템플릿에서 클래스 생성과 클래스의 인스턴스화
클래스 템플릿은 키워드 template를 사용해서 클래스 템플릿을 도입한다.
클래스 템플릿은 클래스를 만드는 모델로, 템플릿 매개변수를 제공해서 구현한다. 예를 들어 Point<int>이다.
서로 다른 템플릿 매개변수를 사용할 때마다, 컴파일러는 새로운 클래스 인스턴스를 생성하는데, 새로운 멤버 함수를 갖는다.
즉, Point<int>::moveTo는 한 함수이고, Point<double>::moveTo는 또 다른 함수이다.
이는 바로 이러한 함수들을 수동으로 작성하면 발생하는 상황이다.
만약 두 개의 다른 소스 파일에서 모두 Point<int>를 사용한다면, 컴파일러와 링커는 그들이 같은 템플릿 인스턴스를 공유하도록 보장할 것이다.
클래스 템플릿은 일종의 청사진이다. 그것은 구체적인 클래스가 아니라, 클래스를 만드는 데 사용되는 일종의 모델이나 프레임워크이다.
클래스 템플릿은 정의 함수와 멤버 변수의 데이터 타입을 템플릿 매개변수 typename T 또는 class T로 허용하는데, 이렇게 하면 필요에 따라 서로 다른 데이터 타입을 사용해서 클래스를 만들 수 있다.
간단히 말하면, 클래스 템플릿은 T를 클래스의 멤버 변수와 멤버 함수 매개변수의 데이터 타입의 자리 표시자로 사용한다. 템플릿 클래스는 자리 표시자를 실제 데이터 타입(int, float 등)으로 교체한 후의 클래스이다.

C++의 Template 중 많은 곳에서 typename과 class라는 두 키워드가 사용되는데, 어떤 때는 이 둘을 서로 바꿔서 쓸 수 있다. 그렇다면 이 두 키워드는 완전히 같을까?
사실 class는 클래스를 정의하는 데 사용된다. 템플릿에 c++ 후를 도입한 후, 처음 템플릿을 정의하는 방법은 template<class T>이다.
여기서 class 키워드는 T가 일종의 타입이라는 것을 나타내는데, 나중에 class가 이 두 곳에서 사용되면 사람을 혼란스럽게 할 수 있다는 점을 피하기 위해서, typename이라는 키워드를 도입했다.
그 작용은 class와 마찬가지로 뒷면의 부호가 일종의 타입임을 나타낸다. 이렇게 하면 템플릿을 정의할 때 다음 방식을 사용할 수 있다.
객체 지향 프로그래밍에서, 보통 클래스를 사용해서 객체를 만드는 과정을 인스턴스화라고 부른다. 예를 들어 Date date = new Date();은 바로 Date 클래스를 사용해서 한 날짜 객체를 만든 것이고, 객체의 인스턴스화라고 부른다.
대부분의 언어에서, 한 객체를 인스턴스화한다는 것은 객체를 위해 메모리 공간을 할당하는 것이거나, 변수 선언 없이 new 생성자명을 직접 사용해서 임시 객체 하나를 만드는 것이다.
type_init과 type_register_static
구체적으로 QEMU에서 템플릿에서 클래스를 생성하고 클래스를 인스턴스화하는 관련 코드를 살펴보자.
QEMU 모듈 하나를 정의하면 type_init을 호출하는데, 그것은 일종의 매크로이다.
#define type_init(function) module_init(function, MODULE_INIT_QOM)
type_init 매크로의 정의에서 볼 수 있듯이, type_init() 안의 매개변수는 한 함수이다.
여기서, type_init(virtio_register_types)를 호출하는 것은 module_init(virtio_register_types, MODULE_INIT_QOM)을 호출하는 것과 같다.
MODULE_INIT_QOM은 일종의 열거값이다.
typedef enum {
MODULE_INIT_MIGRATION,
MODULE_INIT_BLOCK,
MODULE_INIT_OPTS,
MODULE_INIT_QOM,
MODULE_INIT_TRACE,
MODULE_INIT_XEN_BACKEND,
MODULE_INIT_LIBQOS,
MODULE_INIT_FUZZ_TARGET,
MODULE_INIT_MAX
} module_init_type;
module_init도 한 매크로이다.
#ifdef BUILD_DSO
……
#else
/* This should not be used directly. Use block_init etc. instead. */
#define module_init(function, type) \
static void __attribute__((constructor)) do_qemu_init_ ## function(void) \
{ \
register_module_init(function, type); \
}
#endif
void register_module_init(void (*fn)(void), module_init_type type)
{
ModuleEntry *e;
ModuleTypeList *l;
e = g_malloc0(sizeof(*e));
e->init = fn;
e->type = type;
l = find_type(type);
QTAILQ_INSERT_TAIL(l, e, node);
}
type_init 호출 흐름

MODULE_INIT_QOM이라는 유형에 속하는 것은,
Module 목록 ModuleTypeList가 하나 있고,
목록 안에는 한 항목 한 항목씩 ModuleEntry가 있는데,
각 항목의 init 함수를 매개변수 fn 즉 function으로 초기화한다.
여기서는 virtio_register_types이다.
물론, MODULE_INIT_QOM이라는 유형에는 많은 module이 있을 것이다. type_init()를 호출하는 모든 곳에서 MODULE_INIT_QOM 유형의 Module 하나를 등록했다.
각 Module이 어떤 (종류의) 장치를 시뮬레이션해야 한다면, 이러한 장치를 표시하는 TypeImpl이라는 유형을 정의해야 한다.
이것은 사실 일종의 객체 지향 프로그래밍 사상이다. 다만 QEMU에서 사용한 것은 순수 C 언어의 구현일 뿐이다. 그래서 클래스와 객체를 변상해서 구현해야 한다.
static const TypeInfo virtio_blk_info = {
.name = TYPE_VIRTIO_BLK,
.parent = TYPE_VIRTIO_DEVICE,
.instance_size = sizeof(VirtIOBlock),
.instance_init = virtio_blk_instance_init,
.class_init = virtio_blk_class_init,
};
static void virtio_register_types(void)
{
type_register_static(&virtio_blk_info);
}
virtio_register_types 함수는 type_register_static 함수를 호출해서 virtio_blk_info를 등록한다.
이렇게 하면 동적으로 한 클래스를 정의했다고 볼 수 있다.
이 클래스의 이름은 TYPE_VIRTIO_BLK(즉 "virtio-blk-device")이고, 그 부모 클래스는 TYPE_VIRTIO_DEVICE(즉 "virtio-device")이며, 클래스 초기화 함수는 virtio_blk_class_init이다.
만약 이 클래스를 사용해서 한 객체를 선언하면, 객체의 크기는 instance_size 즉 virtio_blk_instance_init이어야 한다.
class_init이 XXXClass 생성
이번에는 QEMU가 템플릿(TypeImpl)으로부터 클래스를 생성하고 인스턴스화하는 관련 코드를 구체적으로 살펴보자
DEVICE_CLASS 매크로
#define TYPE_DEVICE "device"
OBJECT_DECLARE_TYPE(DeviceState, DeviceClass, DEVICE)
DEVICE_CLASS 매크로 전개 흐름

OBJECT_DECLARE_TYPE의 관련 코드는 include/qom/object.h 안에 있다.
/**
* OBJECT_DECLARE_TYPE:
* @InstanceType: instance struct name
* @ClassType: class struct name
* @MODULE_OBJ_NAME: the object name in uppercase with underscore separators
*
* This macro is typically used in a header file, and will:
*
* - create the typedefs for the object and class structs
* - register the type for use with g_autoptr
* - provide three standard type cast functions
*
* The object struct and class struct need to be declared manually.
*/
#define OBJECT_DECLARE_TYPE(InstanceType, ClassType, MODULE_OBJ_NAME) \
typedef struct InstanceType InstanceType; \
typedef struct ClassType ClassType; \
\
G_DEFINE_AUTOPTR_CLEANUP_FUNC(InstanceType, object_unref) \
\
DECLARE_OBJ_CHECKERS(InstanceType, ClassType, \
MODULE_OBJ_NAME, TYPE_##MODULE_OBJ_NAME)
그러면 실제 값을 대입하면
OBJECT_DECLARE_TYPE(DeviceState, DeviceClass, DEVICE)
다음을 얻는다.
typedef struct DeviceState DeviceState;
typedef struct DeviceClass DeviceClass;
G_DEFINE_AUTOPTR_CLEANUP_FUNC(DeviceState, object_unref)
DECLARE_OBJ_CHECKERS(DeviceState, DeviceClass, DEVICE, TYPE_DEVICE)
DECLARE_OBJ_CHECKERS 매크로의 정의는 include/qom/object.h 안에 있다.
/**
* DECLARE_OBJ_CHECKERS:
* @InstanceType: instance struct name
* @ClassType: class struct name
* @OBJ_NAME: the object name in uppercase with underscore separators
* @TYPENAME: type name
*
* Direct usage of this macro should be avoided, and the complete
* OBJECT_DECLARE_TYPE macro is recommended instead.
*
* This macro will provide the three standard type cast functions for a
* QOM type.
*/
#define DECLARE_OBJ_CHECKERS(InstanceType, ClassType, OBJ_NAME, TYPENAME) \
DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME) \
\
DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME)
DECLARE_INSTANCE_CHECKER 매크로의 정의도 같은 파일(include/qom/object.h) 안에 있다.
/**
* DECLARE_INSTANCE_CHECKER:
* @InstanceType: instance struct name
* @OBJ_NAME: the object name in uppercase with underscore separators
* @TYPENAME: type name
*
* Direct usage of this macro should be avoided, and the complete
* OBJECT_DECLARE_TYPE macro is recommended instead.
*
* This macro will provide the instance type cast functions for a
* QOM type.
*/
#define DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME) \
static inline G_GNUC_UNUSED InstanceType * \
OBJ_NAME(const void *obj) \
{ return OBJECT_CHECK(InstanceType, obj, TYPENAME); }
DECLARE_CLASS_CHECKERS 매크로의 정의는 당연히 include/qom/object.h 안에 있고, 다음과 같다.
/**
* DECLARE_CLASS_CHECKERS:
* @ClassType: class struct name
* @OBJ_NAME: the object name in uppercase with underscore separators
* @TYPENAME: type name
*
* Direct usage of this macro should be avoided, and the complete
* OBJECT_DECLARE_TYPE macro is recommended instead.
*
* This macro will provide the class type cast functions for a
* QOM type.
*/
#define DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME) \
static inline G_GNUC_UNUSED ClassType * \
OBJ_NAME##_GET_CLASS(const void *obj) \
{ return OBJECT_GET_CLASS(ClassType, obj, TYPENAME); } \
\
static inline G_GNUC_UNUSED ClassType * \
OBJ_NAME##_CLASS(const void *klass) \
{ return OBJECT_CLASS_CHECK(ClassType, klass, TYPENAME); }
이렇게
#define DECLARE_OBJ_CHECKERS(InstanceType, ClassType, OBJ_NAME, TYPENAME) \
DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME) \
\
DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME)
실제로는 다음과 같다.
static inline G_GNUC_UNUSED InstanceType *OBJ_NAME(const void *obj)
{
return OBJECT_CHECK(InstanceType, obj, TYPENAME);
}
static inline G_GNUC_UNUSED ClassType *OBJ_NAME##_GET_CLASS(const void *obj)
{
return OBJECT_GET_CLASS(ClassType, obj, TYPENAME);
}
static inline G_GNUC_UNUSED ClassType *OBJ_NAME##_CLASS(const void *klass)
{
return OBJECT_CLASS_CHECK(ClassType, klass, TYPENAME);
}
더 나아가, DECLARE_OBJ_CHECKERS()를 호출할 때 전달된 실제 값을 대입하면,
DECLARE_OBJ_CHECKERS(DeviceState, DeviceClass, DEVICE, TYPE_DEVICE)
#define DECLARE_OBJ_CHECKERS(InstanceType, ClassType, OBJ_NAME, TYPENAME) \
DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME) \
\
DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME)
이렇게 얻는다.
DECLARE_INSTANCE_CHECKER(DeviceState, DEVICE, TYPE_DEVICE)
DECLARE_CLASS_CHECKERS(DeviceClass, DEVICE, TYPE_DEVICE)
더 나아가 다음을 얻는다.
static inline G_GNUC_UNUSED DeviceState *DEVICE(const void *obj)
{
return OBJECT_CHECK(DeviceState, obj, TYPE_DEVICE);
}
static inline G_GNUC_UNUSED DeviceClass *DEVICE_GET_CLASS(const void *obj)
{
return OBJECT_GET_CLASS(DeviceClass, obj, TYPE_DEVICE);
}
static inline G_GNUC_UNUSED DeviceClass *DEVICE_CLASS(const void *klass)
{
return OBJECT_CLASS_CHECK(DeviceClass, klass, TYPE_DEVICE);
}
최종적으로 이렇게 얻게된다.
static inline G_GNUC_UNUSED DeviceState *DEVICE(const void *obj)
{
return OBJECT_CHECK(DeviceState, obj, "device");
}
static inline G_GNUC_UNUSED DeviceClass *DEVICE_GET_CLASS(const void *obj)
{
return OBJECT_GET_CLASS(DeviceClass, obj, "device");
}
static inline G_GNUC_UNUSED DeviceClass *DEVICE_CLASS(const void *klass)
{
return OBJECT_CLASS_CHECK(DeviceClass, klass, "device");
}
DEVICE_CLASS의 정의를 명확히 한 후, 메인 코드로 돌아가자.
static void virtio_blk_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);
……
}
안의
DeviceClass *dc = DEVICE_CLASS(klass);
은 바로
static inline G_GNUC_UNUSED DeviceClass *DEVICE_CLASS(const void *klass)
{
return OBJECT_CLASS_CHECK(DeviceClass, klass, "device");
}
DeviceClass *dc = OBJECT_CLASS_CHECK(DeviceClass, klass, "device");
virtio block device 전체 정리

DEVICE_CLASS 매크로 전개

QEMU의 객체 지향 핵심 함수

virtio_blk_class_init·instance_init·device_realize
static void virtio_blk_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);
......
}
VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);
static inline G_GNUC_UNUSED VirtioDeviceClass *VIRTIO_DEVICE_CLASS(const void *klass)
{
return OBJECT_CLASS_CHECK(VirtioDeviceClass, klass, "virtio-device");
}
VirtioDeviceClass *vdc = OBJECT_CLASS_CHECK(VirtioDeviceClass, klass, "virtio-device");
이렇게 virtio_blk_class_init 에서 DeviceClass와 VirtioDeviceClass 생성된다.
type_init()
---> type_register_static()
---> type_register()
---> type_register_internal()
---> type_new()
---> type_table_add()

중점적으로 봐야 할 건 type_register_internal 함수다.
static TypeImpl *type_register_internal(const TypeInfo *info)
{
TypeImpl *ti;
ti = type_new(info);
type_table_add(ti);
return ti;
}
type_register_internal 함수에서는, virtio_blk_info라는 TypeInfo를 기반으로, TypeImpl 하나를 만들어서 이 새로 등록된 클래스를 표현한다. 바꿔 말하면, TypeImpl이야말로 우리가 진짜로 선언하고 싶은 그 클래스다.
QEMU에는 전역 해시 테이블 type_table이 있어서, 정의된 모든 클래스를 저장하는 데 쓴다.
type_register_internal 함수에서는, 먼저 type_new 함수를 호출해서, 전역 테이블에서 이름(이 예에서는 TypeInfo virtio_blk_info)에 따라 이 클래스를 찾는다.
찾으면, 이 클래스가 이미 등록된 적이 있다는 뜻이니까, 에러를 낸다;
못 찾으면, 이 클래스는 새로운 클래스라는 뜻이니까, TypeInfo 안의 정보를 TypeImpl 안에 채워 넣는다.
이 단계까지 오면, 클래스 초기화 class_init(이 예에서는 virtio_blk_class_init) 함수는 아직 호출되지 않았고, 이 클래스는 아직 정의 상태에 있다.
static TypeImpl *type_new(const TypeInfo *info)
{
TypeImpl *ti = g_malloc0(sizeof(*ti));
int i;
g_assert(info->name != NULL);
if (type_table_lookup(info->name) != NULL) {
fprintf(stderr, "Registering `%s' which already exists\n", info->name);
abort();
}
ti->name = g_strdup(info->name);
ti->parent = g_strdup(info->parent);
ti->class_size = info->class_size;
ti->instance_size = info->instance_size;
ti->instance_align = info->instance_align;
ti->class_init = info->class_init;
ti->class_base_init = info->class_base_init;
ti->class_data = info->class_data;
ti->instance_init = info->instance_init;
ti->instance_post_init = info->instance_post_init;
ti->instance_finalize = info->instance_finalize;
ti->abstract = info->abstract;
for (i = 0; info->interfaces && info->interfaces[i].type; i++) {
ti->interfaces[i].typename = g_strdup(info->interfaces[i].type);
}
ti->num_interfaces = i;
return ti;
}
type_new 함수의 동작 흐름

type_new 함수는 시작하자마자 먼저 sizeof(TypeImpl) 크기의 공간을 할당해서, TyptImpl *ti가 이 공간을 가리키도록 한다.
TypeImpl *ti = g_malloc0(sizeof(*ti));
그 다음에는 전달된 함수 파라미터 TypeInfo *info 안의 name 멤버가 비어 있으면 안 된다는 걸 보장해야 한다.
g_assert(info->name != NULL);
이 조건은 일반적으로 만족시킬 수 있다. 예를 들면 virtio_blk_info 안의 name 멤버는 TYPE_VIRTIO_BLK, 즉 "virtio-blk-device"이다.
다음으로 오는 게 type_new 함수에서 가장 핵심적인 코드다.
if (type_table_lookup(info->name) != NULL) {
fprintf(stderr, "Registering `%s' which already exists\n", info->name);
abort();
}
이게 바로 위에서 말한 거다
먼저 type_new 함수를 호출해서, 전역 테이블에서 이름(이 예에서는 TypeInfo virtio_blk_info)에 따라 이 클래스를 찾는다. 찾으면, 이 클래스가 이미 등록된 적이 있다는 뜻이니까, 에러를 낸다;
type_table_lookup 함수도 qom/object.c 안에(바로 type_new 함수 위에) 있고, 코드는 다음과 같다
static TypeImpl *type_table_lookup(const char *name)
{
return g_hash_table_lookup(type_table_get(), name);
}
type_table_get 함수도 qom/object.c 안에 있고, 코드는 다음과 같다
static GHashTable *type_table_get(void)
{
static GHashTable *type_table;
if (type_table == NULL) {
type_table = g_hash_table_new(g_str_hash, g_str_equal);
}
return type_table;
}
이 GHashTable *type_table이 바로 위에서 언급한 전역(해시) 테이블이다.
ti->name = g_strdup(info->name);
ti->parent = g_strdup(info->parent);
ti->class_size = info->class_size;
ti->instance_size = info->instance_size;
ti->instance_align = info->instance_align;
ti->class_init = info->class_init;
ti->class_base_init = info->class_base_init;
ti->class_data = info->class_data;
ti->instance_init = info->instance_init;
ti->instance_post_init = info->instance_post_init;
ti->instance_finalize = info->instance_finalize;
ti->abstract = info->abstract;
for (i = 0; info->interfaces && info->interfaces[i].type; i++) {
ti->interfaces[i].typename = g_strdup(info->interfaces[i].type);
}
ti->num_interfaces = i;
return ti;
이게 바로 위에서 말한 것에 대응된다
못 찾으면, 이 클래스는 새로운 클래스라는 뜻이니까, TypeInfo 안의 정보를 TypeImpl 안에 채워 넣는다.
여기까지 해서, type_new 함수 해석이 끝났다.
static TypeImpl *type_register_internal(const TypeInfo *info)
{
TypeImpl *ti;
ti = type_new(info);
type_table_add(ti);
return ti;
}
앞에서 이미 설명했었다
type_register_internal 함수(qom/object.c 안) 안에서는, virtio_blk_info라는 TypeInfo를 기반으로,
TypeImpl 하나를 만들어서 이 새로 등록된 클래스를 표현한다. 바꿔 말하면, TypeImpl이야말로 우리가 진짜로 선언하고 싶은 그 클래스다.
QEMU에는 전역 해시 테이블 type_table이 있어서, 정의된 모든 클래스를 저장하는 데 쓴다.
type_register_internal 함수에서는, 먼저 type_new 함수를 호출해서, 전역 테이블에서 이름(이 예에서는 TypeInfo virtio_blk_info)에 따라 이 클래스를 찾는다.
찾으면, 이 클래스가 이미 등록된 적이 있다는 뜻이니까, 에러를 낸다; 못 찾으면, 이 클래스는 새로운 클래스라는 뜻이니까, TypeInfo 안의 정보를 TypeImpl 안에 채워 넣는다.
type_new 함수는 이미 TypeInfo virtio_blk_info 안의 정보를 함수 안에서 새로 만든(g_malloc0()을 호출해서 동적으로 만든) TypeImpl 객체에 할당했고, ti가 이 메모리를 가리키게 했다. 다음으로 오는 게 type_table_add 함수를 호출해서, 이 객체를 전역 테이블 type_table에 추가(삽입)하는 거다.
TypeInfo → TypeImpl → 전역 해시 테이블 흐름

static void type_table_add(TypeImpl *ti)
{
assert(!enumerating_types);
g_hash_table_insert(type_table_get(), (void *)ti->name, ti);
}
static GHashTable *type_table_get(void)
{
static GHashTable *type_table;
if (type_table == NULL) {
type_table = g_hash_table_new(g_str_hash, g_str_equal);
}
return type_table;
}
type_table_get 함수는 비교적 간단하고 이해하기 쉽다.
만약 type_table이 NULL이면 즉 전에 만들어진 적이 없으면, g_hash_table_new 함수를 호출해서 만든다. 그 다음에 다시 이 함수를 호출할 때는, 직접 만들어진 type_teble을 반환한다.
g_hash_table_new 함수는 hash 테이블 하나를 만드는 데 쓰이고; g_hash_table_insert 함수는 (만들어진) hash 테이블 안에 항목을 삽입하는 데 쓰이며, 삽입되는 항목은 반드시 하나의 key-value 쌍이어야 한다.
gboolean
g_hash_table_insert (GHashTable *hash_table,
gpointer key,
gpointer value);
GHashTable 안에 새로운 key와 value를 삽입하는데, 만약 오래된 key가 존재하면, value 값이 새로운 value 값으로 덮어쓰여진다.
여기서, key에 대응되는 건 (void *)ti->name이고, value에 대응되는 건 ti 자기 자신이다.
여기까지 해서, TypeImpl로 이미 클래스를 만들었다.
하지만 클래스 초기화 class_init(이 예에서는 virtio_blk_class_init) 함수는 아직 호출되지 않았고, 이 클래스는 아직 정의 상태에 있다(type_init 함수는 클래스를 등록하는 데 쓰이고, 각 계층마다 class_init이 있어서, TypeImpl로 xxxClass를 생성하는 데 쓴다).
virtio_blk_device_realize와 VirtIODevice 구조
앞에서 QEMU 클래스 메커니즘이 템플릿(TypeImpl)으로부터 클래스와 인스턴스를 생성하는 흐름을 설명했다. 이번부터 시작해서, 계속 아래로 블록 디바이스 가상화의 메커니즘과 흐름을 설명한다.
다시 한번 TYPE_VIRTIO_BLK 계층의 클래스 선언을 돌아보자,
static const TypeInfo virtio_blk_info = {
.name = TYPE_VIRTIO_BLK,
.parent = TYPE_VIRTIO_DEVICE,
.instance_size = sizeof(VirtIOBlock),
.instance_init = virtio_blk_instance_init,
.class_init = virtio_blk_class_init,
};
virtio_blk_info 안의 class_init이 가리키는 함수는 virtio_blk_class_init이다.
static void virtio_blk_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);
device_class_set_props(dc, virtio_blk_properties);
dc->vmsd = &vmstate_virtio_blk;
set_bit(DEVICE_CATEGORY_STORAGE, dc->categories);
vdc->realize = virtio_blk_device_realize;
vdc->unrealize = virtio_blk_device_unrealize;
vdc->get_config = virtio_blk_update_config;
vdc->set_config = virtio_blk_set_config;
vdc->get_features = virtio_blk_get_features;
vdc->set_status = virtio_blk_set_status;
vdc->reset = virtio_blk_reset;
vdc->save = virtio_blk_save_device;
vdc->load = virtio_blk_load_device;
vdc->start_ioeventfd = virtio_blk_data_plane_start;
vdc->stop_ioeventfd = virtio_blk_data_plane_stop;
}
virtio_blk_class_init 함수 안에서, VirtioDeviceClass의 realize 함수를 virtio_blk_device_realize()로 정의한다.
static void virtio_blk_device_realize(DeviceState *dev, Error **errp)
{
VirtIODevice *vdev = VIRTIO_DEVICE(dev);
VirtIOBlock *s = VIRTIO_BLK(dev);
VirtIOBlkConf *conf = &s->conf;
Error *err = NULL;
unsigned i;
if (!conf->conf.blk) {
error_setg(errp, "drive property not set");
return;
}
if (!blk_is_inserted(conf->conf.blk)) {
error_setg(errp, "Device needs media, but drive is empty");
return;
}
if (conf->num_queues == VIRTIO_BLK_AUTO_NUM_QUEUES) {
conf->num_queues = 1;
}
if (!conf->num_queues) {
error_setg(errp, "num-queues property must be larger than 0");
return;
}
if (conf->queue_size <= 2) {
error_setg(errp, "invalid queue-size property (%" PRIu16 "), "
"must be > 2", conf->queue_size);
return;
}
if (!is_power_of_2(conf->queue_size) ||
conf->queue_size > VIRTQUEUE_MAX_SIZE) {
error_setg(errp, "invalid queue-size property (%" PRIu16 "), "
"must be a power of 2 (max %d)",
conf->queue_size, VIRTQUEUE_MAX_SIZE);
return;
}
if (!blkconf_apply_backend_options(&conf->conf,
!blk_supports_write_perm(conf->conf.blk),
true, errp)) {
return;
}
s->original_wce = blk_enable_write_cache(conf->conf.blk);
if (!blkconf_geometry(&conf->conf, NULL, 65535, 255, 255, errp)) {
return;
}
if (!blkconf_blocksizes(&conf->conf, errp)) {
return;
}
BlockDriverState *bs = blk_bs(conf->conf.blk);
if (bs->bl.zoned != BLK_Z_NONE) {
virtio_add_feature(&s->host_features, VIRTIO_BLK_F_ZONED);
if (bs->bl.zoned == BLK_Z_HM) {
virtio_clear_feature(&s->host_features, VIRTIO_BLK_F_DISCARD);
}
}
if (virtio_has_feature(s->host_features, VIRTIO_BLK_F_DISCARD) &&
(!conf->max_discard_sectors ||
conf->max_discard_sectors > BDRV_REQUEST_MAX_SECTORS)) {
error_setg(errp, "invalid max-discard-sectors property (%" PRIu32 ")"
", must be between 1 and %d",
conf->max_discard_sectors, (int)BDRV_REQUEST_MAX_SECTORS);
return;
}
if (virtio_has_feature(s->host_features, VIRTIO_BLK_F_WRITE_ZEROES) &&
(!conf->max_write_zeroes_sectors ||
conf->max_write_zeroes_sectors > BDRV_REQUEST_MAX_SECTORS)) {
error_setg(errp, "invalid max-write-zeroes-sectors property (%" PRIu32
"), must be between 1 and %d",
conf->max_write_zeroes_sectors,
(int)BDRV_REQUEST_MAX_SECTORS);
return;
}
s->config_size = virtio_get_config_size(&virtio_blk_cfg_size_params,
s->host_features);
virtio_init(vdev, VIRTIO_ID_BLOCK, s->config_size);
s->blk = conf->conf.blk;
s->rq = NULL;
s->sector_mask = (s->conf.conf.logical_block_size / BDRV_SECTOR_SIZE) - 1;
for (i = 0; i < conf->num_queues; i++) {
virtio_add_queue(vdev, conf->queue_size, virtio_blk_handle_output);
}
qemu_coroutine_inc_pool_size(conf->num_queues * conf->queue_size / 2);
virtio_blk_data_plane_create(vdev, conf, &s->dataplane, &err);
if (err != NULL) {
error_propagate(errp, err);
for (i = 0; i < conf->num_queues; i++) {
virtio_del_queue(vdev, i);
}
virtio_cleanup(vdev);
return;
}
/*
* This must be after virtio_init() so virtio_blk_dma_restart_cb() gets
* called after ->start_ioeventfd() has already set blk's AioContext.
*/
s->change =
qdev_add_vm_change_state_handler(dev, virtio_blk_dma_restart_cb, s);
blk_ram_registrar_init(&s->blk_ram_registrar, s->blk);
blk_set_dev_ops(s->blk, &virtio_block_ops, s);
blk_iostatus_enable(s->blk);
add_boot_device_lchs(dev, "/disk@0,0",
conf->conf.lcyls,
conf->conf.lheads,
conf->conf.lsecs);
}
virtio_blk_device_realize 함수의 주요 동작

virtio_blk_device_realize 함수 안에서는, 먼저 virtio_init 함수를 통해 VirtIODevice 구조를 초기화한다.
virtio_init(vdev, VIRTIO_ID_BLOCK, s->config_size);
void virtio_init(VirtIODevice *vdev, uint16_t device_id, size_t config_size)
{
BusState *qbus = qdev_get_parent_bus(DEVICE(vdev));
VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
int i;
int nvectors = k->query_nvectors ? k->query_nvectors(qbus->parent) : 0;
if (nvectors) {
vdev->vector_queues =
g_malloc0(sizeof(*vdev->vector_queues) * nvectors);
}
vdev->start_on_kick = false;
vdev->started = false;
vdev->vhost_started = false;
vdev->device_id = device_id;
vdev->status = 0;
qatomic_set(&vdev->isr, 0);
vdev->queue_sel = 0;
vdev->config_vector = VIRTIO_NO_VECTOR;
vdev->vq = g_new0(VirtQueue, VIRTIO_QUEUE_MAX);
vdev->vm_running = runstate_is_running();
vdev->broken = false;
for (i = 0; i < VIRTIO_QUEUE_MAX; i++) {
vdev->vq[i].vector = VIRTIO_NO_VECTOR;
vdev->vq[i].vdev = vdev;
vdev->vq[i].queue_index = i;
vdev->vq[i].host_notifier_enabled = false;
}
vdev->name = virtio_id_to_name(device_id);
vdev->config_len = config_size;
if (vdev->config_len) {
vdev->config = g_malloc0(config_size);
} else {
vdev->config = NULL;
}
vdev->vmstate = qdev_add_vm_change_state_handler(DEVICE(vdev),
virtio_vmstate_change, vdev);
vdev->device_endian = virtio_default_endian();
vdev->use_guest_notifier_mask = true;
}
virtio_init 함수 코드에서 볼 수 있듯이, VirtIODevice 구조(VirtIODevice *vdev) 안에는 VirtQueue 배열이 하나 있는데, 이게 바로 virtio 안에서 프론트엔드와 백엔드가 서로 데이터를 전송하는 큐이고, 최대 VIRTIO_QUEUE_MAX개까지 있다.
vdev->vq = g_new0(VirtQueue, VIRTIO_QUEUE_MAX);
vdev->vm_running = runstate_is_running();
vdev->broken = false;
for (i = 0; i < VIRTIO_QUEUE_MAX; i++) {
vdev->vq[i].vector = VIRTIO_NO_VECTOR;
vdev->vq[i].vdev = vdev;
vdev->vq[i].queue_index = i;
vdev->vq[i].host_notifier_enabled = false;
}
VirtIODevice와 VirtQueue 관계

이 점은 VirtIODevice 구조의 정의에서도 볼 수 있다.
/**
* struct VirtIODevice - common VirtIO structure
* @name: name of the device
* @status: VirtIO Device Status field
*
*/
struct VirtIODevice
{
DeviceState parent_obj;
const char *name;
uint8_t status;
uint8_t isr;
uint16_t queue_sel;
/**
* These fields represent a set of VirtIO features at various
* levels of the stack. @host_features indicates the complete
* feature set the VirtIO device can offer to the driver.
* @guest_features indicates which features the VirtIO driver has
* selected by writing to the feature register. Finally
* @backend_features represents everything supported by the
* backend (e.g. vhost) and could potentially be a subset of the
* total feature set offered by QEMU.
*/
uint64_t host_features;
uint64_t guest_features;
uint64_t backend_features;
size_t config_len;
void *config;
uint16_t config_vector;
uint32_t generation;
int nvectors;
VirtQueue *vq;
MemoryListener listener;
uint16_t device_id;
/* @vm_running: current VM running state via virtio_vmstate_change() */
bool vm_running;
bool broken; /* device in invalid state, needs reset */
bool use_disabled_flag; /* allow use of 'disable' flag when needed */
bool disabled; /* device in temporarily disabled state */
/**
* @use_started: true if the @started flag should be used to check the
* current state of the VirtIO device. Otherwise status bits
* should be checked for a current status of the device.
* @use_started is only set via QMP and defaults to true for all
* modern machines (since 4.1).
*/
bool use_started;
bool started;
bool start_on_kick; /* when virtio 1.0 feature has not been negotiated */
bool disable_legacy_check;
bool vhost_started;
VMChangeStateEntry *vmstate;
char *bus_name;
uint8_t device_endian;
/**
* @user_guest_notifier_mask: gate usage of ->guest_notifier_mask() callback.
* This is used to suppress the masking of guest updates for
* vhost-user devices which are asynchronous by design.
*/
bool use_guest_notifier_mask;
AddressSpace *dma_as;
QLIST_HEAD(, VirtQueue) vector_queues;
QTAILQ_ENTRY(VirtIODevice) next;
/**
* @config_notifier: the event notifier that handles config events
*/
EventNotifier config_notifier;
bool device_iotlb_enabled;
};
그 중에서 중점적으로 다음 한 줄의 정의에 주목해보자.
VirtQueue *vq;
VIRTIO_QUEUE_MAX는 매크로 하나인데, 그 정의는 include/hw/virtio/virtio.h 안에 있고, 다음과 같다.
#define VIRTIO_QUEUE_MAX 1024
즉, virtio 안에서 프론트엔드와 백엔드가 서로 데이터를 전송하는 큐는 최대 1024개까지다.
지금까지의 모든 흐름을 시간 순서대로 정리하면 다음과 같다.

핵심 개념

- 정의 상태: TypeInfo로 정적 정의 → type_init → type_register_static → type_new → type_table_add → 전역 해시 테이블 type_table에 TypeImpl로 등록된다.
- Class 객체 생성: class_init (예: virtio_blk_class_init)이 호출되면서 DEVICE_CLASS, VIRTIO_DEVICE_CLASS 같은 매크로를 통해 DeviceClass, VirtioDeviceClass가 생성된다.
- Instance 객체 생성: instance_init (예: virtio_blk_instance_init)이 호출되면서 실제 객체 VirtIOBlock이 생성된다.
- Realize 단계: virtio_blk_device_realize 함수가 호출되면서 virtio_init을 통해 VirtIODevice 구조가 초기화되고, 최대 1024개까지의 VirtQueue가 할당된다. 이게 바로 virtio의 프론트엔드(Guest)와 백엔드(Host/QEMU) 사이에 데이터를 주고받는 큐다.
num_queues 할당과 virtio_add_queue
먼저 이 conf->num_queues가 언제, 어디서 값이 할당되는지 살펴보자.
virtio_blk_device_realize 함수 안에서, 관련된 코드는 딱 한 군데뿐인데, 아래와 같다.
static void virtio_blk_device_realize(DeviceState *dev, Error **errp)
{
VirtIODevice *vdev = VIRTIO_DEVICE(dev);
VirtIOBlock *s = VIRTIO_BLK(dev);
VirtIOBlkConf *conf = &s->conf;
Error *err = NULL;
unsigned i;
if (!conf->conf.blk) {
error_setg(errp, "drive property not set");
return;
}
if (!blk_is_inserted(conf->conf.blk)) {
error_setg(errp, "Device needs media, but drive is empty");
return;
}
if (conf->num_queues == VIRTIO_BLK_AUTO_NUM_QUEUES) { // ← 여기 주목
conf->num_queues = 1; // ← 여기 주목
}
if (!conf->num_queues) {
error_setg(errp, "num-queues property must be larger than 0");
return;
}
conf는 s->conf에서 온 거고, s는 DeviceState *dev를 통해 얻어진 거다.
VirtIOBlock *s = VIRTIO_BLK(dev);

VIRTIO_BLK() 정의 관련 코드는 include/hw/virtio/virtio-blk.h에 있고, 아래와 같다.
#define TYPE_VIRTIO_BLK "virtio-blk-device"
OBJECT_DECLARE_SIMPLE_TYPE(VirtIOBlock, VIRTIO_BLK)
VIRTIO_BLK()의 의미는 이전 글에서 다룬 DEVICE_CLASS(), VIRTIO_DEVICE_CLASS()와 기본적으로 똑같다.
VirtIOBlkConf의 정의도 include/hw/virtio/virtio-blk.h에 있고, 아래와 같다.
struct VirtIOBlkConf
{
BlockConf conf;
IOThread *iothread;
char *serial;
uint32_t request_merging;
uint16_t num_queues;
uint16_t queue_size;
bool seg_max_adjust;
bool report_discard_granularity;
uint32_t max_discard_sectors;
uint32_t max_write_zeroes_sectors;
bool x_enable_wce_if_config_wce;
};
지금까지 본 s->conf.num_queues 값이 정해지는 경로는 다음과 같다(QEMU 8.1.3 기준).
먼저 hw/block/virtio-blk.c에서 QOM 프로퍼티로 등록되어 있다
- DEFINE_PROP_UINT16("num-queues", VirtIOBlock, conf.num_queues, VIRTIO_BLK_AUTO_NUM_QUEUES)
- 즉 기본값은 VIRTIO_BLK_AUTO_NUM_QUEUES(=UINT16_MAX)이고, 사용자가 -device virtio-blk-pci,num-queues=N으로 지정하면 그 값이 들어간다.
- 값을 지정하지 않아 AUTO 상태로 남으면, hw/virtio/virtio-blk-pci.c의 virtio_blk_pci_realize 함수에서 virtio_pci_optimal_num_queues()로 vCPU 수에 맞는 최적값을 채운다.
- 그 뒤 virtio_blk_device_realize에서 혹시 아직 AUTO이면 최종적으로 1로 설정한다. 코드는 아래와 같다.
static void virtio_blk_pci_realize(VirtIOPCIProxy *vpci_dev, Error **errp)
{
VirtIOBlkPCI *dev = VIRTIO_BLK_PCI(vpci_dev);
DeviceState *vdev = DEVICE(&dev->vdev);
VirtIOBlkConf *conf = &dev->vdev.conf;
if (conf->num_queues == VIRTIO_BLK_AUTO_NUM_QUEUES) {
conf->num_queues = virtio_pci_optimal_num_queues(0);
}
if (vpci_dev->nvectors == DEV_NVECTORS_UNSPECIFIED) {
vpci_dev->nvectors = conf->num_queues + 1;
}
qdev_realize(vdev, BUS(&vpci_dev->bus), errp);
}
num_queues 할당 두 가지 경로

virtio_add_queue 함수 분석
virtio_blk_device_realize 함수 안에 있는 virtio_add_queue 함수를 분석한다.
for (i = 0; i < conf->num_queues; i++) {
virtio_add_queue(vdev, conf->queue_size, virtio_blk_handle_output);
}
virtio_add_queue 함수는 hw/virtio/virtio.c에 있고, 코드는 아래와 같다.
VirtQueue *virtio_add_queue(VirtIODevice *vdev, int queue_size,
VirtIOHandleOutput handle_output)
{
int i;
for (i = 0; i < VIRTIO_QUEUE_MAX; i++) {
if (vdev->vq[i].vring.num == 0)
break;
}
if (i == VIRTIO_QUEUE_MAX || queue_size > VIRTQUEUE_MAX_SIZE)
abort();
vdev->vq[i].vring.num = queue_size;
vdev->vq[i].vring.num_default = queue_size;
vdev->vq[i].vring.align = VIRTIO_PCI_VRING_ALIGN;
vdev->vq[i].handle_output = handle_output;
vdev->vq[i].used_elems = g_new0(VirtQueueElement, queue_size);
return &vdev->vq[i];
}
virtio_add_queue 동작 흐름

각 VirtQueue 안에는 vring이 하나씩 있는데, 이 큐 안의 데이터를 관리하는 데 쓰인다. 이 점은 VirtQueue 구조체의 정의에서도 확인할 수 있다. VirtQueue 구조체의 정의는 hw/virtio/virtio.c에 있고, 아래와 같다.
struct VirtQueue
{
VRing vring;
VirtQueueElement *used_elems;
/* Next head to pop */
uint16_t last_avail_idx;
bool last_avail_wrap_counter;
/* Last avail_idx read from VQ. */
uint16_t shadow_avail_idx;
bool shadow_avail_wrap_counter;
uint16_t used_idx;
bool used_wrap_counter;
/* Last used index value we have signalled on */
uint16_t signalled_used;
/* Last used index value we have signalled on */
bool signalled_used_valid;
/* Notification enabled? */
bool notification;
uint16_t queue_index;
unsigned int inuse;
uint16_t vector;
VirtIOHandleOutput handle_output;
VirtIODevice *vdev;
EventNotifier guest_notifier;
EventNotifier host_notifier;
bool host_notifier_enabled;
QLIST_ENTRY(VirtQueue) node;
};
보다시피, VirtQueue의 첫 번째 멤버가 바로 VRing vring이다. VRing 구조체의 정의는 hw/virtio/virtio.c에 있고, 아래와 같다.
typedef struct VRing
{
unsigned int num;
unsigned int num_default;
unsigned int align;
hwaddr desc;
hwaddr avail;
hwaddr used;
VRingMemoryRegionCaches *caches;
} VRing;
여기서 중점적으로 봐야 할 건 struct VirtQueue의 멤버 VirtIOHandleOutput handle_output이다.
이게 대응하는 실제 인자가 virtio_blk_handle_output이다.
즉 handle_output이 가리키는 게 virtio_blk_handle_output 함수다.
여기서는 일단 복선만 깔아두고, 나중에 다른 글에서 이 virtio_blk_handle_output 함수를 다시 보게 된다. 지금은 데이터 쓰기를 처리하는 데 쓰인다는 정도만 알고 있으면 된다.
for (i = 0; i < conf->num_queues; i++) {
virtio_add_queue(vdev, conf->queue_size, virtio_blk_handle_output);
}
VirtIODevice, VirtQueue, VRing 사이의 관계

3) QEMU 시작 과정: -drive 파싱과 drive_new
qemu-system-x86_64는 QEMU를 기반으로 한 명령줄 도구로, x86 아키텍처의 컴퓨터 시스템을 시뮬레이션하는 데 쓰인다. 아래는 자주 쓰이는 qemu-system-x86_64 명령과 그 사용법이다.
1. 가상머신 하나 시작하기
qemu-system-x86_64 -m 2048 -hda /path/to/disk_image.qcow2
-m 파라미터는 가상머신의 메모리 크기를 지정하고, -hda 파라미터는 디스크 이미지 파일의 경로를 지정한다.
2. VNC로 가상머신에 원격 접속하기
qemu-system-x86_64 -m 2048 -hda /path/to/disk_image.qcow2 -vnc :1
-vnc 파라미터는 VNC 프로토콜로 가상머신에 접속하도록 지정하고, 접속 포트는 5901로 지정한다.
3. KVM 가속 활성화
qemu-system-x86_64 -enable-kvm -m 2048 -hda /path/to/disk_image.qcow2
-enable-kvm 파라미터는 KVM 하드웨어 가속을 활성화하도록 지정한다.
4. 가상머신의 네트워크 설정
qemu-system-x86_64 -m 2048 -hda /path/to/disk_image.qcow2 -net nic -net user,hostfwd=tcp::2222-:22
-net 파라미터는 가상머신의 네트워크 설정을 지정하고, -nic 파라미터는 가상머신이 사용할 네트워크 카드 타입을 지정한다. -net user 파라미터는 가상머신이 유저 모드 네트워크를 사용하도록 지정하고, hostfwd 파라미터는 호스트의 2222 포트를 가상머신의 22 포트로 매핑한다.
5. ISO 이미지 파일 로드
qemu-system-x86_64 -m 2048 -cdrom /path/to/iso_image.iso -boot d
-cdrom 파라미터는 ISO 이미지 파일을 로드하도록 지정하고, -boot 파라미터는 광학 드라이브로 부팅하도록 지정한다.

QEMU는 다양한 블록 장치 시뮬레이션을 제공하는데, IDE 장치, SCSI 장치, 플로피, USB, virtio 디스크 등이 포함된다. QEMU 명령줄 도구에서, 게스트의 스토리지를 설정하는 데 주로 아래와 같은 파라미터들이 쓰인다.
(1) -hda <file>
file 이미지 파일을 게스트의 첫 번째 IDE 장치(번호 0)로 쓴다. 게스트 안에서는 /dev/hda 장치(게스트가 PIIX_IDE 드라이버를 쓸 때)나 /dev/sda 장치(게스트가 ata_piix 드라이버를 쓸 때)로 나타난다. 만약 -hda나 -hdb 같은 파라미터를 지정하지 않으면, 기본값은 -hda 파라미터다.
이 외에, 호스트의 하드디스크(예: /dev/sda)를 -hda의 file 파라미터로 써서, 전체 디스크를 게스트의 첫 번째 IDE 장치로 시뮬레이션할 수도 있다.
(2) -hdb <file>
file을 게스트의 두 번째 IDE 장치(번호 1)로 쓴다. 게스트 안에서는 /dev/hdb나 /dev/sdb 장치로 나타난다.
(3) -hdc <file>
file을 게스트의 세 번째 IDE 장치(번호 2)로 쓴다. 게스트 안에서는 /dev/hdc나 /dev/sdc 장치로 나타난다.
(4) -hdd <file>
file을 게스트의 네 번째 IDE 장치(번호 3)로 쓴다. 게스트 안에서는 /dev/hdd나 /dev/sdd 장치로 나타난다.
(5) -fda <file>
file을 게스트의 첫 번째 플로피(floppy) 장치(번호 0)로 쓴다. 게스트 안에서는 /dev/fd0 장치로 나타난다. 호스트의 플로피 드라이브(/dev/fd0)를 -fda의 file로 써도 된다.
(6) -fdb <file>
file을 게스트의 두 번째 플로피(floppy) 장치(번호 1)로 쓴다. 게스트 안에서는 /dev/fd1 장치로 나타난다.
(7) -cdrom <file>
file을 게스트의 CD-ROM으로 쓴다. 게스트 안에서는 보통 /dev/cdrom 장치로 나타난다. 호스트의 광학 드라이브(/dev/cdrom)를 -cdrom의 file 파라미터로 써도 된다. 물리 광학 드라이브의 디스크나 디스크 안의 ISO 이미지 파일로 게스트 운영체제를 설치할 때는 보통 "-cdrom" 파라미터를 쓴다.
주의:
-cdrom 파라미터는 -hdc 파라미터와 동시에 쓸 수 없는데, "-cdrom"이 바로 게스트의 세 번째 IDE 장치이기 때문이다.
(8) -mtdblock <file>
file 파일을 게스트가 자체적으로 갖춘 Flash 저장 장치(보통 말하는 플래시)로 쓴다.
(9) -sd <file>
file 파일을 게스트의 SD 카드(Secure Digital Card)로 쓴다.
(10) -pflash <file>
file 파일을 게스트의 병렬 Flash 저장 장치(Parallel Flash Memory)로 쓴다.
호스트 파일 ↔ 게스트 장치 매핑 관계

-drive 파라미터로 스토리지 드라이버 자세히 설정하기
지금까지 설명한 스토리지의 기본 설정 옵션 외에, QEMU는 "-driver" 파라미터를 하나 더 제공하는데, 이걸로 스토리지 드라이버 하나를 자세히 정의할 수 있다. 이 파라미터의 구체적인 형식은 아래와 같다.
-drive option [, option [, option [,...]]]
게스트에 새로운 드라이버 하나를 정의해주는 거고, 다음과 같은 옵션(option)들이 있다.
(1) file=<file>
file 파일을 이미지 파일로 써서, 게스트의 드라이버에 로드한다.
(2) if=<interface>
드라이버의 인터페이스 타입을 지정한다. 사용 가능한 타입에는 ide, scsi, sd, mtd, floppy, pflash, virtio 등이 있다.
(3) bus=<bus>
드라이버가 게스트 안에서 갖는 버스 번호와 유닛 번호를 설정한다.
(4) index=<index>
같은 인터페이스의 드라이버 중에서의 인덱스 번호를 설정한다.
(5) media=<media>
드라이버에서 미디어의 타입을 설정하는데, 선택 가능한 값은 "disk" 또는 "cdrom"이다.
(6) snapshot=<snapshot>
"-snapshot" 옵션을 활성화할지 설정하는데, 선택 가능한 값은 "on" 또는 "off"다. snapshot이 활성화되면, QEMU는 디스크 데이터의 변경을 이미지 파일에 다시 쓰지 않고, 임시 파일에 쓴다.
(7) cache=<cache>
호스트가 블록 장치 데이터에 접근할 때의 cache 상황을 설정하는데, "none"(또는 "off"), "writeback", "writethrough" 등으로 설정할 수 있다. 기본값은 "writethrough", 즉 "직접 쓰기 모드(write-through 모드)"다.
"writethrough"(즉 직접 쓰기 모드)는 write를 호출해서 데이터를 쓸 때, 데이터를 디스크 캐시(disk cache)와 백엔드 블록 장치(block device)에 동시에 쓰는 거다. 장점은 작업이 간단하다는 거고, 단점은 데이터 쓰기 속도가 비교적 느리다는 거다.
반면 "writeback"(즉 라이트백 모드)는 write를 호출해서 데이터를 쓸 때, 데이터를 디스크 캐시에 쓰고 바로 리턴하는 거고, 캐시에서 밀려나는 데이터가 있을 때만 변경된 데이터를 백엔드 블록 장치에 쓴다. 장점은 쓰기 속도가 빠르다는 거고, 단점은 데이터 업데이트 후 백엔드 스토리지에 쓰기 전에 시스템이 다운되면 데이터를 복구할 방법이 없다는 거다.
"writethrough"와 "writeback"은 데이터에 접근할 때 모두 캐시를 최대한 활용한다. 만약 "cache=none"으로 설정했다면, 즉 캐시를 끄는 방식이면, QEMU는 이미지 파일을 열 때 open 시스템 콜을 호출하면서 "O_DIRECT" 플래그를 쓰기 때문에, 데이터를 읽고 쓸 때 캐시를 우회해서 블록 장치에서 직접 읽고 쓴다. 몇몇 블록 장치 파일(qcow2 형식 파일 같은 거)은 "writethrough" 모드에서 성능이 매우 나쁘다, 만약 이때 성능이 정확성보다 더 요구된다면, "writeback" 모드를 쓰는 걸 추천한다.
캐시 모드 3가지 비교

(8) aio=<aio>
비동기 IO(Asynchronous IO) 방식을 선택하는데, "threads"와 "native" 두 가지 값을 고를 수 있다. 기본값은 "threads"인데, 즉 스레드 풀로 비동기 IO를 처리하게 하는 거다. 반면 "native"는 "cache=none"인 경우에만 쓰이는데, 바로 Linux 네이티브 AIO를 쓰는 거다.
(9) format=<format>
사용하는 디스크 포맷을 지정한다. 기본적으로 QEMU가 자동으로 디스크 포맷을 감지한다.
(10) serial=<serial>
장치에 할당되는 시리얼 번호를 지정한다.
(11) addr=<addr>
드라이버 컨트롤러에 할당되는 PCI 주소다. 이 옵션은 virtio 인터페이스를 쓸 때만 적용된다.
(12) id=<name>
이 드라이버의 ID를 설정한다. 이 ID는 QEMU monitor에서 "info block" 명령을 써서 조회할 수 있다.
(13) readonly=on|off
이 드라이버가 읽기 전용인지 설정한다.
QEMU 시작 과정의 블록 장치 가상화 - 함수 호출 흐름
지금까지 "스토리지의 기본 설정 옵션"과 "스토리지 드라이버를 자세히 설정하는 -drive 파라미터"를 확인했는데, 목적은 QEMU 시작 과정의 블록 장치 가상화를 더 잘 분석하기 위해서다.
하드디스크의 가상화에 대해, QEMU의 시작 파라미터에서 관련된 (일반적으로 있을 수 있는) 건 다음 두 줄이다
-drive file=/var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/disk,if=none,id=drive-virtio-disk0,
-device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x4,drive=drive-virtio-disk0,bootindex=1
-drive file=/var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/disk,if=none,id=drive-virtio-disk0,format=qcow2,cache=none
-device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x4,drive=drive-virtio-disk0,bootindex=1
위 코드의 첫 번째 줄은 호스트 하드디스크 위에 있는 파일 하나를 지정했는데, 파일의 포맷은 qcow2다. 호스트의 파일 하나에 대해, QEMU가 게스트의 하드디스크 하나로 시뮬레이션할 수 있다.
-drive와 -device 두 줄이 하는 일

스토리지 드라이버를 자세히 설정하는 -drive 파라미터
지난 회차에서 설명한 스토리지의 기본 설정 옵션 외에, QEMU는 "-driver" 파라미터를 하나 더 제공하는데, 이걸로 스토리지 드라이버 하나를 자세히 정의할 수 있다. 이 파라미터의 구체적인 형식은 아래와 같다.
-drive option [, option [, option [,...]]]게스트에 새로운 드라이버 하나를 정의해주는 거고, 다음과 같은 옵션(option)들이 있다
(1) file=<file>
file 파일을 이미지 파일로 써서, 게스트의 드라이버에 로드한다.
(2) if=<interface>
드라이버의 인터페이스 타입을 지정한다. 사용 가능한 타입에는 ide, scsi, sd, mtd, floppy, pflash, virtio 등이 있다.
(9) format=<format>
사용하는 디스크 포맷을 지정한다. 기본적으로 QEMU가 자동으로 디스크 포맷을 감지한다.
그리고 위 코드의 두 번째 줄에서는 사용하는 드라이버가 virtio-blk 드라이버라고 명시했다.
QEMU 시작의 main 함수 안에서 (새 버전(8.1.3/8.1.4)에서는 이미 qemu_create_early_backends()로 옮겨졌고, 이 함수는 qemu_init 함수에서 호출된다), 블록 장치 초기화는 configure_blockdev 함수를 통해 시작된다.
static void qemu_create_early_backends(void)
if (dpy.has_gl && dpy.gl != DISPLAYGL_MODE_OFF && display_opengl == 0) {
object_option_foreach_add(object_create_early);
/* spice needs the timers to be initialized by this point */
/* spice must initialize before audio as it changes the default audiodev */
/* spice must initialize before chardevs (for spicevmc and spiceport) */
qemu_spice.init();
qemu_opts_foreach(qemu_find_opts("chardev"),
chardev_init_func, NULL, &error_fatal);
#ifdef CONFIG_VIRTFS
qemu_opts_foreach(qemu_find_opts("fsdev"),
fsdev_init_func, NULL, &error_fatal);
#endif
/*
* Note: we need to create audio and block backends before
* setting machine properties, so they can be referred to.
*/
configure_blockdev(&bdo_queue, machine_class, snapshot);
if (!audio_init_audiodevs()) {
exit(1);
}
}
configure_blockdev 함수도 softmmu/vl.c에 있고, 코드는 아래와 같다.
static void configure_blockdev(BlockdevOptionsQueue *bdo_queue,
MachineClass *machine_class, int snapshot)
{
/*
* If the currently selected machine wishes to override the
* units-per-bus property of its default HBA interface type, do so
* now.
*/
if (machine_class->units_per_default_bus) {
override_max_devs(machine_class->block_default_type,
machine_class->units_per_default_bus);
}
/* open the virtual block devices */
while (!QSIMPLEQ_EMPTY(bdo_queue)) {
BlockdevOptionsQueueEntry *bdo = QSIMPLEQ_FIRST(bdo_queue);
QSIMPLEQ_REMOVE_HEAD(bdo_queue, entry);
loc_push_restore(&bdo->loc);
qmp_blockdev_add(bdo->bdo, &error_fatal);
loc_pop(&bdo->loc);
qapi_free_BlockdevOptions(bdo->bdo);
g_free(bdo);
}
if (snapshot) {
qemu_opts_foreach(qemu_find_opts("drive"), drive_enable_snapshot,
NULL, NULL);
}
if (qemu_opts_foreach(qemu_find_opts("drive"), drive_init_func,
&machine_class->block_default_type, &error_fatal)) {
/* We printed help */
exit(0);
}
default_drive(default_cdrom, snapshot, machine_class->block_default_type, 2,
CDROM_OPTS);
default_drive(default_floppy, snapshot, IF_FLOPPY, 0, FD_OPTS);
default_drive(default_sdcard, snapshot, IF_SD, 0, SD_OPTS);
}
configure_blockdev 함수 안에서, -drive 이 파라미터에 대한 파싱을 볼 수 있고, 또 이 장치를 초기화하기 위해 drive_init_func 함수를 호출한다.
if (qemu_opts_foreach(qemu_find_opts("drive"), drive_init_func,
&machine_class->block_default_type, &error_fatal)) {
/* We printed help */
exit(0);
}
drive_init_func 함수는 softmmu/vl.c에 있고, 코드는 아래와 같다.
static int drive_init_func(void *opaque, QemuOpts *opts, Error **errp)
{
BlockInterfaceType *block_default_type = opaque;
return drive_new(opts, *block_default_type, errp) == NULL;
}
drive_init_func 함수 안에서, drive_new 함수를 호출해서 장치 하나를 만든다. drive_new 함수는 blockdev.c에 있고, 코드는 아래와 같다.
DriveInfo *drive_new(QemuOpts *all_opts, BlockInterfaceType block_default_type,
Error **errp)
{
const char *value;
BlockBackend *blk;
DriveInfo *dinfo = NULL;
QDict *bs_opts;
QemuOpts *legacy_opts;
DriveMediaType media = MEDIA_DISK;
BlockInterfaceType type;
int max_devs, bus_id, unit_id, index;
const char *werror, *rerror;
bool read_only = false;
bool copy_on_read;
const char *filename;
int i;
GLOBAL_STATE_CODE();
/* Change legacy command line options into QMP ones */
static const struct {
const char *from;
const char *to;
} opt_renames[] = {
{ "iops", "throttling.iops-total" },
{ "iops_rd", "throttling.iops-read" },
{ "iops_wr", "throttling.iops-write" },
{ "bps", "throttling.bps-total" },
{ "bps_rd", "throttling.bps-read" },
{ "bps_wr", "throttling.bps-write" },
{ "iops_max", "throttling.iops-total-max" },
{ "iops_rd_max", "throttling.iops-read-max" },
{ "iops_wr_max", "throttling.iops-write-max" },
{ "bps_max", "throttling.bps-total-max" },
{ "bps_rd_max", "throttling.bps-read-max" },
{ "bps_wr_max", "throttling.bps-write-max" },
{ "iops_size", "throttling.iops-size" },
{ "group", "throttling.group" },
{ "readonly", BDRV_OPT_READ_ONLY },
};
for (i = 0; i < ARRAY_SIZE(opt_renames); i++) {
if (!qemu_opt_rename(all_opts, opt_renames[i].from,
opt_renames[i].to, errp)) {
return NULL;
}
}
value = qemu_opt_get(all_opts, "cache");
if (value) {
int flags = 0;
bool writethrough;
if (bdrv_parse_cache_mode(value, &flags, &writethrough) != 0) {
error_setg(errp, "invalid cache option");
return NULL;
}
/* Specific options take precedence */
if (!qemu_opt_get(all_opts, BDRV_OPT_CACHE_WB)) {
qemu_opt_set_bool(all_opts, BDRV_OPT_CACHE_WB,
!writethrough, &error_abort);
}
if (!qemu_opt_get(all_opts, BDRV_OPT_CACHE_DIRECT)) {
qemu_opt_set_bool(all_opts, BDRV_OPT_CACHE_DIRECT,
!!(flags & BDRV_O_NOCACHE), &error_abort);
}
if (!qemu_opt_get(all_opts, BDRV_OPT_CACHE_NO_FLUSH)) {
qemu_opt_set_bool(all_opts, BDRV_OPT_CACHE_NO_FLUSH,
!!(flags & BDRV_O_NO_FLUSH), &error_abort);
}
qemu_opt_unset(all_opts, "cache");
}
/* Get a QDict for processing the options */
bs_opts = qdict_new();
qemu_opts_to_qdict(all_opts, bs_opts);
legacy_opts = qemu_opts_create(&qemu_legacy_drive_opts, NULL, 0,
&error_abort);
if (!qemu_opts_absorb_qdict(legacy_opts, bs_opts, errp)) {
goto fail;
}
/* Media type */
value = qemu_opt_get(legacy_opts, "media");
if (value) {
if (!strcmp(value, "disk")) {
media = MEDIA_DISK;
} else if (!strcmp(value, "cdrom")) {
media = MEDIA_CDROM;
read_only = true;
} else {
error_setg(errp, "'%s' invalid media", value);
goto fail;
}
}
/* copy-on-read is disabled with a warning for read-only devices */
read_only |= qemu_opt_get_bool(legacy_opts, BDRV_OPT_READ_ONLY, false);
copy_on_read = qemu_opt_get_bool(legacy_opts, "copy-on-read", false);
if (read_only && copy_on_read) {
warn_report("disabling copy-on-read on read-only drive");
copy_on_read = false;
}
qdict_put_str(bs_opts, BDRV_OPT_READ_ONLY, read_only ? "on" : "off");
qdict_put_str(bs_opts, "copy-on-read", copy_on_read ? "on" : "off");
/* Controller type */
value = qemu_opt_get(legacy_opts, "if");
if (value) {
for (type = 0;
type < IF_COUNT && strcmp(value, if_name[type]);
type++) {
}
if (type == IF_COUNT) {
error_setg(errp, "unsupported bus type '%s'", value);
goto fail;
}
} else {
type = block_default_type;
}
/* Device address specified by bus/unit or index.
* If none was specified, try to find the first free one. */
bus_id = qemu_opt_get_number(legacy_opts, "bus", 0);
unit_id = qemu_opt_get_number(legacy_opts, "unit", -1);
index = qemu_opt_get_number(legacy_opts, "index", -1);
max_devs = if_max_devs[type];
if (index != -1) {
if (bus_id != 0 || unit_id != -1) {
error_setg(errp, "index cannot be used with bus and unit");
goto fail;
}
bus_id = drive_index_to_bus_id(type, index);
unit_id = drive_index_to_unit_id(type, index);
}
if (unit_id == -1) {
unit_id = 0;
while (drive_get(type, bus_id, unit_id) != NULL) {
unit_id++;
if (max_devs && unit_id >= max_devs) {
unit_id -= max_devs;
bus_id++;
}
}
}
if (max_devs && unit_id >= max_devs) {
error_setg(errp, "unit %d too big (max is %d)", unit_id, max_devs - 1);
goto fail;
}
if (drive_get(type, bus_id, unit_id) != NULL) {
error_setg(errp, "drive with bus=%d, unit=%d (index=%d) exists",
bus_id, unit_id, index);
goto fail;
}
/* no id supplied -> create one */
if (qemu_opts_id(all_opts) == NULL) {
char *new_id;
const char *mediastr = "";
if (type == IF_IDE || type == IF_SCSI) {
mediastr = (media == MEDIA_CDROM) ? "-cd" : "-hd";
}
if (max_devs) {
new_id = g_strdup_printf("%s%i%s%i", if_name[type], bus_id,
mediastr, unit_id);
} else {
new_id = g_strdup_printf("%s%s%i", if_name[type],
mediastr, unit_id);
}
qdict_put_str(bs_opts, "id", new_id);
g_free(new_id);
}
/* Add virtio block device */
if (type == IF_VIRTIO) {
QemuOpts *devopts;
devopts = qemu_opts_create(qemu_find_opts("device"), NULL, 0,
&error_abort);
qemu_opt_set(devopts, "driver", "virtio-blk", &error_abort);
qemu_opt_set(devopts, "drive", qdict_get_str(bs_opts, "id"),
&error_abort);
}
filename = qemu_opt_get(legacy_opts, "file");
/* Check werror/rerror compatibility with if=... */
werror = qemu_opt_get(legacy_opts, "werror");
if (werror != NULL) {
if (type != IF_IDE && type != IF_SCSI && type != IF_VIRTIO &&
type != IF_NONE) {
error_setg(errp, "werror is not supported by this bus type");
goto fail;
}
qdict_put_str(bs_opts, "werror", werror);
}
rerror = qemu_opt_get(legacy_opts, "rerror");
if (rerror != NULL) {
if (type != IF_IDE && type != IF_VIRTIO && type != IF_SCSI &&
type != IF_NONE) {
error_setg(errp, "rerror is not supported by this bus type");
goto fail;
}
qdict_put_str(bs_opts, "rerror", rerror);
}
/* Actual block device init: Functionality shared with blockdev-add */
blk = blockdev_init(filename, bs_opts, errp);
bs_opts = NULL;
if (!blk) {
goto fail;
}
/* Create legacy DriveInfo */
dinfo = g_malloc0(sizeof(*dinfo));
dinfo->opts = all_opts;
dinfo->type = type;
dinfo->bus = bus_id;
dinfo->unit = unit_id;
blk_set_legacy_dinfo(blk, dinfo);
switch (type) {
case IF_IDE:
case IF_SCSI:
case IF_XEN:
case IF_NONE:
dinfo->media_cd = media == MEDIA_CDROM;
break;
default:
break;
}
fail:
qemu_opts_del(legacy_opts);
qobject_unref(bs_opts);
return dinfo;
}
시작부터 drive_new까지 함수 호출 흐름

drive_new 함수 자세히 분석
지난번 다룬 QEMU 시작 과정에서 호출되는 함수 흐름은 다음과 같다
main 함수
---> qemu_init 함수
---> qemu_create_early_backends() 함수
---> configure_blockdev 함수
---> drive_init_func 함수
---> driver_new 함수

static void configure_blockdev(BlockdevOptionsQueue *bdo_queue,
MachineClass *machine_class, int snapshot)
{
/*
* If the currently selected machine wishes to override the
* units-per-bus property of its default HBA interface type, do so
* now.
*/
if (machine_class->units_per_default_bus) {
override_max_devs(machine_class->block_default_type,
machine_class->units_per_default_bus);
}
/* open the virtual block devices */
while (!QSIMPLEQ_EMPTY(bdo_queue)) {
BlockdevOptionsQueueEntry *bdo = QSIMPLEQ_FIRST(bdo_queue);
QSIMPLEQ_REMOVE_HEAD(bdo_queue, entry);
loc_push_restore(&bdo->loc);
qmp_blockdev_add(bdo->bdo, &error_fatal);
loc_pop(&bdo->loc);
qapi_free_BlockdevOptions(bdo->bdo);
g_free(bdo);
}
if (snapshot) {
qemu_opts_foreach(qemu_find_opts("drive"), drive_enable_snapshot,
NULL, NULL);
}
if (qemu_opts_foreach(qemu_find_opts("drive"), drive_init_func,
&machine_class->block_default_type, &error_fatal)) {
/* We printed help */
exit(0);
}
default_drive(default_cdrom, snapshot, machine_class->block_default_type, 2,
CDROM_OPTS);
default_drive(default_floppy, snapshot, IF_FLOPPY, 0, FD_OPTS);
default_drive(default_sdcard, snapshot, IF_SD, 0, SD_OPTS);
}
drive_new 함수는 장치 하나를 만드는 데 쓰인다. 그 안에서 QEMU의 시작 파라미터를 파싱하게 된다. virtio에 대해 말하자면, device 파라미터를 파싱해서 driver를 "virtio-blk"로 설정한다.
/* Add virtio block device */
if (type == IF_VIRTIO) {
QemuOpts *devopts;
devopts = qemu_opts_create(qemu_find_opts("device"),
NULL, 0,
&error_abort);
qemu_opt_set(devopts, "driver", "virtio-blk", &error_abort);
qemu_opt_set(devopts, "drive", qdict_get_str(bs_opts, "id"),
&error_abort);
}
시작 명령줄 중 다음 줄에 대응한다.
-device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x4,drive=drive-virtio-disk0,bootindex=1
다음으로, drive_new 함수는 file 파라미터를 파싱하는데, 호스트의 그 파일을 가리키는 거다.
filename = qemu_opt_get(legacy_opts, "file");
시작 명령줄 중 다음 줄에 대응한다.
-drive file=/var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/disk,if=none,id=drive-virtio-disk0,format=qcow2,cache=none
다음으로, drive_new 함수는 blockdev_init 함수를 호출해서, 파라미터에 따라 초기화를 진행한다.
/* Actual block device init: Functionality shared with blockdev-add */
blk = blockdev_init(filename, bs_opts, errp);
bs_opts = NULL;
if (!blk) {
goto fail;
}
마지막으로, drive_new 함수는 DriverInfo 구조체 객체를 하나 만들어서, 이 새로 만든 장치를 관리한다.
DriveInfo *dinfo = NULL;
……
/* Create legacy DriveInfo */
dinfo = g_malloc0(sizeof(*dinfo));
dinfo->opts = all_opts;
dinfo->type = type;
dinfo->bus = bus_id;
dinfo->unit = unit_id;
blk_set_legacy_dinfo(blk, dinfo);
switch (type) {
case IF_IDE:
case IF_SCSI:
case IF_XEN:
case IF_NONE:
dinfo->media_cd = media == MEDIA_CDROM;
break;
default:
break;
}
drive_new 함수의 4가지 핵심 작업

명령줄 파라미터와 drive_new 처리 매핑

전체 흐름 - 명령줄에서 VirtQueue 생성까지

핵심 자료구조 관계

두 단계의 관점 차이

핵심 개념 정리

- -drive vs -device: -drive는 호스트 파일을 어떻게 다룰지(file, format, cache)를 정하고, -device는 게스트에 어떤 드라이버로 보일지(virtio-blk-pci 등)를 정한다. 둘은 id로 연결된다.
- 두 단계로 나뉜 흐름: QEMU 시작 시점에는 configure_blockdev → drive_new → blockdev_init로 호스트 측 블록 백엔드를 준비한다. 그 다음 장치 실체화 시점에 virtio_blk_pci_realize → virtio_blk_device_realize로 게스트 측 virtio 장치를 만든다.
- num_queues 결정 순서: PCI 단의 virtio_blk_pci_realize가 AUTO면 virtio_pci_optimal_num_queues로 설정하고, 그래도 AUTO면 공통 단의 virtio_blk_device_realize가 1로 설정한다.
- virtio_add_queue 호출은 num_queues만큼: 각 호출이 VirtQueue 슬롯 하나를 차지하고, 그 안의 VRing을 초기화하고, handle_output을 virtio_blk_handle_output 함수로 등록한다.
- handle_output이 진짜 데이터 처리 진입점: 게스트가 큐에 요청을 넣을 때마다 호출될 콜백.
4) 디스크 이미지 열기 (bdrv_open → qcow2 / raw)
drive_new부터 시작해서 디스크 파일이 실제로 열리기까지의 호출 체인 전체를 먼저 한눈에 보자. QEMU 명령행의 -drive 옵션이 어떻게 가상 머신 내부의 블록 디바이스로 변환되는지, 그 흐름이다.

노란색은 command line, 초록색은 프론트엔드(blockdev 레벨), 파란색은 백엔드(bdrv 레벨), 주황색은 포맷별 드라이버
drive_new에서 blockdev_init으로
이전에 drive_new 함수를 대략 분석했는데, 이번에는 drive_new 함수에서 호출하는 blockdev_init 함수에 대한 분석을 중점적으로 다룬다.
/* Actual block device init: Functionality shared with blockdev-add */
blk = blockdev_init(filename, bs_opts, errp);
bs_opts = NULL;
if (!blk) {
goto fail;
}
blockdev_init 함수는 blockdev.c 안에 있다.
/* Takes the ownership of bs_opts */
static BlockBackend *blockdev_init(const char *file, QDict *bs_opts,
Error **errp)
{
const char *buf;
int bdrv_flags = 0;
int on_read_error, on_write_error;
bool account_invalid, account_failed;
BlockBackend *blk;
BlockDriverState *bs;
ThrottleConfig cfg;
int snapshot = 0;
Error *error = NULL;
QemuOpts *opts;
QDict *interval_dict = NULL;
QList *interval_list = NULL;
const char *id;
BlockdevDetectZeroesOptions detect_zeroes =
BLOCKDEV_DETECT_ZEROES_OPTIONS_OFF;
const char *throttling_group = NULL;
/* Check common options by copying from bs_opts to opts, all other options
* stay in bs_opts for processing by bdrv_open(). */
id = qdict_get_try_str(bs_opts, "id");
opts = qemu_opts_create(&qemu_common_drive_opts, id, 1, errp);
if (!opts) {
goto err_no_opts;
}
if (!qemu_opts_absorb_qdict(opts, bs_opts, errp)) {
goto early_err;
}
if (id) {
qdict_del(bs_opts, "id");
}
/* extract parameters */
snapshot = qemu_opt_get_bool(opts, "snapshot", 0);
account_invalid = account_get_opt(opts, "stats-account-invalid");
account_failed = account_get_opt(opts, "stats-account-failed");
writethrough = !qemu_opt_get_bool(opts, BDRV_OPT_CACHE_WB, true);
id = qemu_opts_id(opts);
qdict_extract_subqdict(bs_opts, &interval_dict, "stats-intervals.");
qdict_array_split(interval_dict, &interval_list);
if (qdict_size(interval_dict) != 0) {
error_setg(errp, "Invalid option stats-intervals.%s",
qdict_first(interval_dict)->key);
goto early_err;
}
extract_common_blockdev_options(opts, &bdrv_flags, &throttling_group, &cfg,
&detect_zeroes, &error);
if (error) {
error_propagate(errp, error);
goto early_err;
}
if ((buf = qemu_opt_get(opts, "format")) != NULL) {
if (is_help_option(buf)) {
qemu_printf("Supported formats:");
bdrv_iterate_format(bdrv_format_print, NULL, false);
qemu_printf("\nSupported formats (read-only):");
bdrv_iterate_format(bdrv_format_print, NULL, true);
qemu_printf("\n");
goto early_err;
}
if (qdict_haskey(bs_opts, "driver")) {
error_setg(errp, "Cannot specify both 'driver' and 'format'");
goto early_err;
}
qdict_put_str(bs_opts, "driver", buf);
}
on_write_error = BLOCKDEV_ON_ERROR_ENOSPC;
if ((buf = qemu_opt_get(opts, "werror")) != NULL) {
on_write_error = parse_block_error_action(buf, 0, &error);
if (error) {
error_propagate(errp, error);
goto early_err;
}
}
on_read_error = BLOCKDEV_ON_ERROR_REPORT;
if ((buf = qemu_opt_get(opts, "rerror")) != NULL) {
on_read_error = parse_block_error_action(buf, 1, &error);
if (error) {
error_propagate(errp, error);
goto early_err;
}
}
if (snapshot) {
bdrv_flags |= BDRV_O_SNAPSHOT;
}
read_only = qemu_opt_get_bool(opts, BDRV_OPT_READ_ONLY, false);
/* init */
if ((!file || !*file) && !qdict_size(bs_opts)) {
BlockBackendRootState *blk_rs;
blk = blk_new(qemu_get_aio_context(), 0, BLK_PERM_ALL);
blk_rs = blk_get_root_state(blk);
blk_rs->open_flags = bdrv_flags | (read_only ? 0 : BDRV_O_RDWR);
blk_rs->detect_zeroes = detect_zeroes;
qobject_unref(bs_opts);
} else {
if (file && !*file) {
file = NULL;
}
/* bdrv_open() defaults to the values in bdrv_flags (for compatibility
* with other callers) rather than what we want as the real defaults.
* Apply the defaults here instead. */
qdict_set_default_str(bs_opts, BDRV_OPT_CACHE_DIRECT, "off");
qdict_set_default_str(bs_opts, BDRV_OPT_CACHE_NO_FLUSH, "off");
qdict_set_default_str(bs_opts, BDRV_OPT_READ_ONLY,
read_only ? "on" : "off");
qdict_set_default_str(bs_opts, BDRV_OPT_AUTO_READ_ONLY, "on");
assert((bdrv_flags & BDRV_O_CACHE_MASK) == 0);
if (runstate_check(RUN_STATE_INMIGRATE)) {
bdrv_flags |= BDRV_O_INACTIVE;
}
blk = blk_new_open(file, NULL, bs_opts, bdrv_flags, errp);
if (!blk) {
goto err_no_bs_opts;
}
bs = blk_bs(blk);
bs->detect_zeroes = detect_zeroes;
block_acct_setup(blk_get_stats(blk), account_invalid, account_failed);
if (!parse_stats_intervals(blk_get_stats(blk), interval_list, errp)) {
blk_unref(blk);
blk = NULL;
goto err_no_bs_opts;
}
}
/* disk I/O throttling */
if (throttle_enabled(&cfg)) {
if (!throttling_group) {
throttling_group = id;
}
blk_io_limits_enable(blk, throttling_group);
blk_set_io_limits(blk, &cfg);
}
blk_set_enable_write_cache(blk, !writethrough);
blk_set_on_error(blk, on_read_error, on_write_error);
if (!monitor_add_blk(blk, id, errp)) {
blk_unref(blk);
blk = NULL;
goto err_no_bs_opts;
}
err_no_bs_opts:
qemu_opts_del(opts);
qobject_unref(interval_dict);
qobject_unref(interval_list);
return blk;
early_err:
qemu_opts_del(opts);
qobject_unref(interval_dict);
qobject_unref(interval_list);
err_no_opts:
qobject_unref(bs_opts);
return NULL;
}
blockdev_init 함수 안의 분기 구조를 그림으로 정리해보면 이렇게 된다. file이 비어있는지 아닌지에 따라 두 갈래로 갈린다.

여기서, file은 blockdev_init 함수의 입력 파라미터다.
static BlockBackend *blockdev_init(const char *file, QDict *bs_opts,
Error **errp)
이에 대응되는 실제 인자는 지난번에 다뤘다.
다음으로, drive_new 함수는 file 파라미터를 파싱하는데, 이것이 바로 호스트 머신 상의 그 파일을 가리킨다. 코드 조각은 아래와 같다
filename = qemu_opt_get(legacy_opts, "file");
시작 명령행에서 대응되는 부분은
-drive file=/var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/disk,if=none,id=drive-virtio-disk0,format=qcow2,cache=none
다음으로, drive_new 함수는 blockdev_init 함수를 호출해서 파라미터에 따라 초기화를 진행한다. 코드 조각은 아래와 같다/* Actual block device init: Functionality shared with blockdev-add */ blk = blockdev_init(filename, bs_opts, errp); bs_opts = NULL; if (!blk) { goto fail; }
실제 인자 filename은 당연히 비어 있지 않으니, blk_new_open 함수를 호출해서 호스트 머신 상의 하드 디스크 파일(명령행에 있는 그 /var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/disk가 돼야 한다)을 연다.
blk_new_open 함수는 block/block-backend.c 안에 있고, 코드는 아래와 같다.
/*
* Creates a new BlockBackend, opens a new BlockDriverState, and connects both.
* By default, the new BlockBackend is in the main AioContext, but if the
* parameters connect it with any existing node in a different AioContext, it
* may end up there instead.
*
* Just as with bdrv_open(), after having called this function the reference to
* @options belongs to the block layer (even on failure).
*
* Called without holding an AioContext lock.
*
* TODO: Remove @filename and @flags; it should be possible to specify a whole
* BDS tree just by specifying the options QDict (or @reference,
* alternatively). At the time of adding this function, this is not possible,
* though, so callers of this function have to be able to specify @filename and
* @flags.
*/
BlockBackend *blk_new_open(const char *filename, const char *reference,
QDict *options, int flags, Error **errp)
{
BlockBackend *blk;
BlockDriverState *bs;
AioContext *ctx;
uint64_t perm = 0;
uint64_t shared = BLK_PERM_ALL;
GLOBAL_STATE_CODE();
/*
* blk_new_open() is mainly used in .bdrv_create implementations and the
* tools where sharing isn't a major concern because the BDS stays private
* and the file is generally not supposed to be used by a second process,
* so we just request permission according to the flags.
*
* The exceptions are xen_disk and blockdev_init(); in these cases, the
* caller of blk_new_open() doesn't make use of the permissions, but they
* shouldn't hurt either. We can still share everything here because the
* guest devices will add their own blockers if they can't share.
*/
if ((flags & BDRV_O_NO_IO) == 0) {
perm |= BLK_PERM_CONSISTENT_READ;
if (flags & BDRV_O_RDWR) {
perm |= BLK_PERM_WRITE;
}
}
if (flags & BDRV_O_RESIZE) {
perm |= BLK_PERM_RESIZE;
}
if (flags & BDRV_O_NO_SHARE) {
shared = BLK_PERM_CONSISTENT_READ | BLK_PERM_WRITE_UNCHANGED;
}
aio_context_acquire(qemu_get_aio_context());
bs = bdrv_open(filename, reference, options, flags, errp);
aio_context_release(qemu_get_aio_context());
if (!bs) {
return NULL;
}
/* bdrv_open() could have moved bs to a different AioContext */
ctx = bdrv_get_aio_context(bs);
blk = blk_new(bdrv_get_aio_context(bs), perm, shared);
blk->perm = perm;
blk->shared_perm = shared;
aio_context_acquire(ctx);
blk_insert_bs(blk, bs, errp);
bdrv_unref(bs);
aio_context_release(ctx);
if (!blk->root) {
blk_unref(blk);
return NULL;
}
return blk;
}
blk_new_open이 하는 일을 정리하면 이렇게 된다. 핵심은 BlockBackend(blk)와 BlockDriverState(bs)를 만들고 둘을 연결하는 것

위에서 이미 언급했듯이, blk_new_open 함수의 역할은 호스트 머신 상의 하드 디스크 파일(즉 명령행 파라미터 file에 대응되는 그 /var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/disk 파일)을 여는 것이다. 반환 결과는 BlockBackend *blk이고, 이 blk는 함수 안에서 bdrv_open 함수를 통해 얻은 것이다.
여기서 이름만 봐도 알 수 있듯이, 이미 백엔드 디바이스의 범주에 속한다. blk_new_open 함수의 주석에 분명하게 적혀 있다.
새로운 BlockBackend를 생성하고, 새로운 BlockDriverState를 열고, 그리고 둘을 연결한다.
여기서 BlockBackend, BlockDriverState, BlockDriver 세 개의 핵심 데이터 구조 관계를 짚어두자. 이게 QEMU 블록 레이어의 기본 그림이다

bdrv_open 함수는 block.c 안에 있고, 코드는 아래와 같다.
/* The caller must always hold the main AioContext lock. */
BlockDriverState *bdrv_open(const char *filename, const char *reference,
QDict *options, int flags, Error **errp)
{
GLOBAL_STATE_CODE();
return bdrv_open_inherit(filename, reference, options, flags, NULL,
NULL, 0, errp);
}
bdrv_open에서 bdrv_open_driver로
지금까지 blockdev_init 함수를 분석하기 시작했고, 그 안에서 호출되는 blk_new_open 함수에 대해 다뤘다.
이 함수의 역할은 호스트 머신 상의 하드 디스크 파일을 여는 것이다. blk_new_open 함수 안에서는 또 bdrv_open 함수를 호출하는데, 이것은 block.c 안에 있고 코드는 아래와 같다
static BlockBackend *blockdev_init(const char *file, QDict *bs_opts,
Error **errp)
bdrv_open_inherit 함수도 block.c 안에 있고, 코드는 아래와 같다.
/*
* Opens a disk image (raw, qcow2, vmdk, ...)
*
* options is a QDict of options to pass to the block drivers, or NULL for an
* empty set of options. The reference to the QDict belongs to the block layer
* after the call (even on failure), so if the caller intends to reuse the
* dictionary, it needs to use qobject_ref() before calling bdrv_open.
*
* If *pbs is NULL, a new BDS will be created with a pointer to it stored there.
* If it is not NULL, the referenced BDS will be reused.
*
* The reference parameter may be used to specify an existing block device which
* should be opened. If specified, neither options nor a filename may be given,
* nor can an existing BDS be reused (that is, *pbs has to be NULL).
*
* The caller must always hold the main AioContext lock.
*/
static BlockDriverState * no_coroutine_fn
bdrv_open_inherit(const char *filename, const char *reference, QDict *options,
int flags, BlockDriverState *parent,
const BdrvChildClass *child_class, BdrvChildRole child_role,
Error **errp)
{
int ret;
BlockBackend *file = NULL;
BlockDriverState *bs;
BlockDriver *drv = NULL;
BdrvChild *child;
const char *drvname;
const char *backing;
Error *local_err = NULL;
QDict *snapshot_options = NULL;
int snapshot_flags = 0;
AioContext *ctx = qemu_get_aio_context();
assert(!child_class || !flags);
assert(!child_class == !parent);
GLOBAL_STATE_CODE();
assert(!qemu_in_coroutine());
if (reference) {
bool options_non_empty = options ? qdict_size(options) : false;
qobject_unref(options);
if (filename || options_non_empty) {
error_setg(errp, "Cannot reference an existing block device with "
"additional options or a new filename");
return NULL;
}
bs = bdrv_lookup_bs(reference, reference, errp);
if (!bs) {
return NULL;
}
bdrv_ref(bs);
return bs;
}
bs = bdrv_new();
/* NULL means an empty set of options */
if (options == NULL) {
options = qdict_new();
}
/* json: syntax counts as explicit options, as if in the QDict */
parse_json_protocol(options, &filename, &local_err);
if (local_err) {
goto fail;
}
bs->explicit_options = qdict_clone_shallow(options);
if (child_class) {
bool parent_is_format;
if (parent->drv) {
parent_is_format = parent->drv->is_format;
} else {
/*
* parent->drv is not set yet because this node is opened for
* (potential) format probing. That means that @parent is going
* to be a format node.
*/
parent_is_format = true;
}
bs->inherits_from = parent;
child_class->inherit_options(child_role, parent_is_format,
&flags, options,
parent->open_flags, parent->options);
}
ret = bdrv_fill_options(&options, filename, &flags, &local_err);
if (ret < 0) {
goto fail;
}
/*
* Set the BDRV_O_RDWR and BDRV_O_ALLOW_RDWR flags.
* Caution: getting a boolean member of @options requires care.
* When @options come from -blockdev or blockdev_add, members are
* typed according to the QAPI schema, but when they come from
* -drive, they're all QString.
*/
if (g_strcmp0(qdict_get_try_str(options, BDRV_OPT_READ_ONLY), "on") &&
!qdict_get_try_bool(options, BDRV_OPT_READ_ONLY, false)) {
flags |= (BDRV_O_RDWR | BDRV_O_ALLOW_RDWR);
} else {
flags &= ~BDRV_O_RDWR;
}
if (flags & BDRV_O_SNAPSHOT) {
snapshot_options = qdict_new();
bdrv_temp_snapshot_options(&snapshot_flags, snapshot_options,
flags, options);
/* Let bdrv_backing_options() override "read-only" */
qdict_del(options, BDRV_OPT_READ_ONLY);
bdrv_inherited_options(BDRV_CHILD_COW, true,
&flags, options, flags, options);
}
bs->open_flags = flags;
bs->options = options;
options = qdict_clone_shallow(options);
/* Find the right image format driver */
/* See cautionary note on accessing @options above */
drvname = qdict_get_try_str(options, "driver");
if (drvname) {
drv = bdrv_find_format(drvname);
if (!drv) {
error_setg(errp, "Unknown driver: '%s'", drvname);
goto fail;
}
}
assert(drvname || !(flags & BDRV_O_PROTOCOL));
/* See cautionary note on accessing @options above */
backing = qdict_get_try_str(options, "backing");
if (qobject_to(QNull, qdict_get(options, "backing")) != NULL ||
(backing && *backing == '\0'))
{
if (backing) {
warn_report("Use of \"backing\": \"\" is deprecated; "
"use \"backing\": null instead");
}
flags |= BDRV_O_NO_BACKING;
qdict_del(bs->explicit_options, "backing");
qdict_del(bs->options, "backing");
qdict_del(options, "backing");
}
/* Open image file without format layer. This BlockBackend is only used for
* probing, the block drivers will do their own bdrv_open_child() for the
* same BDS, which is why we put the node name back into options. */
if ((flags & BDRV_O_PROTOCOL) == 0) {
BlockDriverState *file_bs;
file_bs = bdrv_open_child_bs(filename, options, "file", bs,
&child_of_bds, BDRV_CHILD_IMAGE,
true, &local_err);
if (local_err) {
goto fail;
}
if (file_bs != NULL) {
/* Not requesting BLK_PERM_CONSISTENT_READ because we're only
* looking at the header to guess the image format. This works even
* in cases where a guest would not see a consistent state. */
ctx = bdrv_get_aio_context(file_bs);
aio_context_acquire(ctx);
file = blk_new(ctx, 0, BLK_PERM_ALL);
blk_insert_bs(file, file_bs, &local_err);
bdrv_unref(file_bs);
aio_context_release(ctx);
if (local_err) {
goto fail;
}
qdict_put_str(options, "file", bdrv_get_node_name(file_bs));
}
}
/* Image format probing */
bs->probed = !drv;
if (!drv && file) {
ret = find_image_format(file, filename, &drv, &local_err);
if (ret < 0) {
goto fail;
}
/*
* This option update would logically belong in bdrv_fill_options(),
* but we first need to open bs->file for the probing to work, while
* opening bs->file already requires the (mostly) final set of options
* so that cache mode etc. can be inherited.
*
* Adding the driver later is somewhat ugly, but it's not an option
* that would ever be inherited, so it's correct. We just need to make
* sure to update both bs->options (which has the full effective
* options for bs) and options (which has file.* already removed).
*/
qdict_put_str(bs->options, "driver", drv->format_name);
qdict_put_str(options, "driver", drv->format_name);
} else if (!drv) {
error_setg(errp, "Must specify either driver or file");
goto fail;
}
/* BDRV_O_PROTOCOL must be set iff a protocol BDS is about to be created */
assert(!!(flags & BDRV_O_PROTOCOL) == !!drv->bdrv_file_open);
/* file must be NULL if a protocol BDS is about to be created
* (the inverse results in an error message from bdrv_open_common()) */
assert(!(flags & BDRV_O_PROTOCOL) || !file);
/* Open the image */
ret = bdrv_open_common(bs, file, options, &local_err);
if (ret < 0) {
goto fail;
}
/* The AioContext could have changed during bdrv_open_common() */
ctx = bdrv_get_aio_context(bs);
if (file) {
aio_context_acquire(ctx);
blk_unref(file);
aio_context_release(ctx);
file = NULL;
}
/* If there is a backing file, use it */
if ((flags & BDRV_O_NO_BACKING) == 0) {
ret = bdrv_open_backing_file(bs, options, "backing", &local_err);
if (ret < 0) {
goto close_and_fail;
}
}
/* Remove all children options and references
* from bs->options and bs->explicit_options */
QLIST_FOREACH(child, &bs->children, next) {
char *child_key_dot;
child_key_dot = g_strdup_printf("%s.", child->name);
qdict_extract_subqdict(bs->explicit_options, NULL, child_key_dot);
qdict_extract_subqdict(bs->options, NULL, child_key_dot);
qdict_del(bs->explicit_options, child->name);
qdict_del(bs->options, child->name);
g_free(child_key_dot);
}
/* Check if any unknown options were used */
if (qdict_size(options) != 0) {
const QDictEntry *entry = qdict_first(options);
if (flags & BDRV_O_PROTOCOL) {
error_setg(errp, "Block protocol '%s' doesn't support the option "
"'%s'", drv->format_name, entry->key);
} else {
error_setg(errp,
"Block format '%s' does not support the option '%s'",
drv->format_name, entry->key);
}
goto close_and_fail;
}
bdrv_parent_cb_change_media(bs, true);
qobject_unref(options);
options = NULL;
/* For snapshot=on, create a temporary qcow2 overlay. bs points to the
* temporary snapshot afterwards. */
if (snapshot_flags) {
BlockDriverState *snapshot_bs;
snapshot_bs = bdrv_append_temp_snapshot(bs, snapshot_flags,
snapshot_options, &local_err);
snapshot_options = NULL;
if (local_err) {
goto close_and_fail;
}
/* We are not going to return bs but the overlay on top of it
* (snapshot_bs); thus, we have to drop the strong reference to bs
* (which we obtained by calling bdrv_new()). bs will not be deleted,
* though, because the overlay still has a reference to it. */
aio_context_acquire(ctx);
bdrv_unref(bs);
aio_context_release(ctx);
bs = snapshot_bs;
}
return bs;
fail:
aio_context_acquire(ctx);
blk_unref(file);
qobject_unref(snapshot_options);
qobject_unref(bs->explicit_options);
qobject_unref(bs->options);
qobject_unref(options);
bs->options = NULL;
bs->explicit_options = NULL;
bdrv_unref(bs);
aio_context_release(ctx);
error_propagate(errp, local_err);
return NULL;
close_and_fail:
aio_context_acquire(ctx);
bdrv_unref(bs);
aio_context_release(ctx);
qobject_unref(snapshot_options);
qobject_unref(options);
error_propagate(errp, local_err);
return NULL;
}
bdrv_open_inherit는 분기가 좀 많아서 한눈에 보기 어려운데, 큰 줄기만 추리면 이런 흐름이다.

bdrv_open_inherit 함수의 역할은 디스크 이미지(raw, qcow2, vmdk 등 포맷)를 여는 것이다. 그중 가장 핵심이 되는 부분은
/* Open the image */
ret = bdrv_open_common(bs, file, options, &local_err);
if (ret < 0) {
goto fail;
}
bdrv_open_common 함수도 block.c 안에 있고, 코드는 아래와 같다.
/*
* Common part for opening disk images and files
*
* Removes all processed options from *options.
*/
static int bdrv_open_common(BlockDriverState *bs, BlockBackend *file,
QDict *options, Error **errp)
{
int ret, open_flags;
const char *filename;
const char *driver_name = NULL;
const char *node_name = NULL;
const char *discard;
QemuOpts *opts;
BlockDriver *drv;
Error *local_err = NULL;
bool ro;
assert(bs->file == NULL);
assert(options != NULL && bs->options != options);
GLOBAL_STATE_CODE();
opts = qemu_opts_create(&bdrv_runtime_opts, NULL, 0, &error_abort);
if (!qemu_opts_absorb_qdict(opts, options, errp)) {
ret = -EINVAL;
goto fail_opts;
}
update_flags_from_options(&bs->open_flags, opts);
driver_name = qemu_opt_get(opts, "driver");
drv = bdrv_find_format(driver_name);
assert(drv != NULL);
bs->force_share = qemu_opt_get_bool(opts, BDRV_OPT_FORCE_SHARE, false);
if (bs->force_share && (bs->open_flags & BDRV_O_RDWR)) {
error_setg(errp,
BDRV_OPT_FORCE_SHARE
"=on can only be used with read-only images");
ret = -EINVAL;
goto fail_opts;
}
if (file != NULL) {
bdrv_refresh_filename(blk_bs(file));
filename = blk_bs(file)->filename;
} else {
/*
* Caution: while qdict_get_try_str() is fine, getting
* non-string types would require more care. When @options
* come from -blockdev or blockdev_add, its members are typed
* according to the QAPI schema, but when they come from
* -drive, they're all QString.
*/
filename = qdict_get_try_str(options, "filename");
}
if (drv->bdrv_needs_filename && (!filename || !filename[0])) {
error_setg(errp, "The '%s' block driver requires a file name",
drv->format_name);
ret = -EINVAL;
goto fail_opts;
}
trace_bdrv_open_common(bs, filename ?: "", bs->open_flags,
drv->format_name);
ro = bdrv_is_read_only(bs);
if (use_bdrv_whitelist && !bdrv_is_whitelisted(drv, ro)) {
if (!ro && bdrv_is_whitelisted(drv, true)) {
ret = bdrv_apply_auto_read_only(bs, NULL, NULL);
} else {
ret = -ENOTSUP;
}
if (ret < 0) {
error_setg(errp,
!ro && bdrv_is_whitelisted(drv, true)
? "Driver '%s' can only be used for read-only devices"
: "Driver '%s' is not whitelisted",
drv->format_name);
goto fail_opts;
}
}
/* bdrv_new() and bdrv_close() make it so */
assert(qatomic_read(&bs->copy_on_read) == 0);
if (bs->open_flags & BDRV_O_COPY_ON_READ) {
if (!ro) {
bdrv_enable_copy_on_read(bs);
} else {
error_setg(errp, "Can't use copy-on-read on read-only device");
ret = -EINVAL;
goto fail_opts;
}
}
discard = qemu_opt_get(opts, BDRV_OPT_DISCARD);
if (discard != NULL) {
if (bdrv_parse_discard_flags(discard, &bs->open_flags) != 0) {
error_setg(errp, "Invalid discard option");
ret = -EINVAL;
goto fail_opts;
}
}
bs->detect_zeroes =
bdrv_parse_detect_zeroes(opts, bs->open_flags, &local_err);
if (local_err) {
error_propagate(errp, local_err);
ret = -EINVAL;
goto fail_opts;
}
if (filename != NULL) {
pstrcpy(bs->filename, sizeof(bs->filename), filename);
} else {
bs->filename[0] = '\0';
}
pstrcpy(bs->exact_filename, sizeof(bs->exact_filename), bs->filename);
/* Open the image, either directly or using a protocol */
open_flags = bdrv_open_flags(bs, bs->open_flags);
node_name = qemu_opt_get(opts, "node-name");
assert(!drv->bdrv_file_open || file == NULL);
ret = bdrv_open_driver(bs, drv, node_name, options, open_flags, errp);
if (ret < 0) {
goto fail_opts;
}
qemu_opts_del(opts);
return 0;
fail_opts:
qemu_opts_del(opts);
return ret;
}
bdrv_open_common 함수 안에서, 디스크 파일의 포맷에 따라 BlockDriver *drv를 얻는다.
static int bdrv_open_common(BlockDriverState *bs, BlockBackend *file,
QDict *options, Error **errp)
{
int ret, open_flags;
const char *filename;
const char *driver_name = NULL;
const char *node_name = NULL;
const char *discard;
QemuOpts *opts;
BlockDriver *drv;
Error *local_err = NULL;
bool ro;
……
driver_name = qemu_opt_get(opts, "driver");
drv = bdrv_find_format(driver_name);
assert(drv != NULL);
……
}
그리고 나서 bdrv_open_driver 함수를 호출하는데, 얻어진 BlockDriver *drv에 따라 디스크 파일을 연다.
assert(!drv->bdrv_file_open || file == NULL);
ret = bdrv_open_driver(bs, drv, node_name, options, open_flags, errp);
if (ret < 0) {
goto fail_opts;
}
bdrv_open_common의 핵심 두 단계를 그림으로 보면 이렇다. "포맷 이름 → 드라이버 객체"로 변환한 다음, 그 드라이버를 들고 bdrv_open_driver로 들어간다.

bdrv_open_driver 함수도 block 안에 있고, 코드는 아래와 같다.
/*
* The caller must always hold @bs AioContext lock, because this function calls
* bdrv_refresh_total_sectors() which polls when called from non-coroutine
* context.
*/
static int no_coroutine_fn GRAPH_UNLOCKED
bdrv_open_driver(BlockDriverState *bs, BlockDriver *drv, const char *node_name,
QDict *options, int open_flags, Error **errp)
{
AioContext *ctx;
Error *local_err = NULL;
int i, ret;
GLOBAL_STATE_CODE();
bdrv_assign_node_name(bs, node_name, &local_err);
if (local_err) {
error_propagate(errp, local_err);
return -EINVAL;
}
bs->drv = drv;
bs->opaque = g_malloc0(drv->instance_size);
if (drv->bdrv_file_open) {
assert(!drv->bdrv_needs_filename || bs->filename[0]);
ret = drv->bdrv_file_open(bs, options, open_flags, &local_err);
} else if (drv->bdrv_open) {
ret = drv->bdrv_open(bs, options, open_flags, &local_err);
} else {
ret = 0;
}
if (ret < 0) {
if (local_err) {
error_propagate(errp, local_err);
} else if (bs->filename[0]) {
error_setg_errno(errp, -ret, "Could not open '%s'", bs->filename);
} else {
error_setg_errno(errp, -ret, "Could not open image");
}
goto open_failed;
}
assert(!(bs->supported_read_flags & ~BDRV_REQ_MASK));
assert(!(bs->supported_write_flags & ~BDRV_REQ_MASK));
/*
* Always allow the BDRV_REQ_REGISTERED_BUF optimization hint. This saves
* drivers that pass read/write requests through to a child the trouble of
* declaring support explicitly.
*
* Drivers must not propagate this flag accidentally when they initiate I/O
* to a bounce buffer. That case should be rare though.
*/
bs->supported_read_flags |= BDRV_REQ_REGISTERED_BUF;
bs->supported_write_flags |= BDRV_REQ_REGISTERED_BUF;
/* Get the context after .bdrv_open, it can change the context */
ctx = bdrv_get_aio_context(bs);
aio_context_acquire(ctx);
ret = bdrv_refresh_total_sectors(bs, bs->total_sectors);
if (ret < 0) {
error_setg_errno(errp, -ret, "Could not refresh total sector count");
aio_context_release(ctx);
return ret;
}
bdrv_graph_rdlock_main_loop();
bdrv_refresh_limits(bs, NULL, &local_err);
bdrv_graph_rdunlock_main_loop();
aio_context_release(ctx);
if (local_err) {
error_propagate(errp, local_err);
return -EINVAL;
}
assert(bdrv_opt_mem_align(bs) != 0);
assert(bdrv_min_mem_align(bs) != 0);
assert(is_power_of_2(bs->request_alignment));
for (i = 0; i < bs->quiesce_counter; i++) {
if (drv->bdrv_drain_begin) {
drv->bdrv_drain_begin(bs);
}
}
return 0;
open_failed:
bs->drv = NULL;
if (bs->file != NULL) {
bdrv_unref_child(bs, bs->file);
assert(!bs->file);
}
g_free(bs->opaque);
bs->opaque = NULL;
return ret;
}
bdrv_open_driver의 핵심은 결국 함수 포인터 디스패치 한 줄이다. drv->bdrv_open이 raw_open인지 qcow2_open인지에 따라 다른 코드가 실행된다. C 언어로 구현된 "가상 함수 호출"이라고 봐도 된다.

위에서 이미 언급했듯이, 가상 머신의 하드 디스크 파일 포맷은 여러 가지가 있다. 예를 들어 raw, qcow2, vmdk 등이고, 각 포맷마다 각자의 장단점이 있다. 가상 머신을 시작할 때, 자유롭게 지정할 수 있다.
- raw
raw는 원본 디스크 이미지 포맷이다. 베어 디스크 포맷이라고도 부르고, 기본 가상 디스크 포맷이다.
raw는, 본래 의미가 "미가공"이고, 미포맷화의 디스크를 가리키기도 한다.
Linux 안에서, raw는 디스크 이미지의 순수 이진 이미지 포맷이다. 희소 파일(sparse file)을 지원하는 파일 시스템에서는, raw 포맷의 이미지 파일은 데이터 기록을 위해 실제로 사용되는 공간만 차지한다.
이 포맷의 장점은 단순하고, 다른 에뮬레이터(가상화 환경) 안으로 가져오고 내보내기가 쉽다는 것이다. 만약 파일 시스템이 holes을 지원한다면 (예를 들어 linux 안의 ext2, ext3, ext4나 windows 상의 ntfs),
이런 환경에서 raw 디스크에 대해서는 이미 데이터가 기록된 섹터만 저장 공간을 차지하므로, "qemu-img info" 명령으로 raw 디스크의 실제 크기를 볼 수 있고, "ls -ls"로도 볼 수 있다.
- qcow2
qcow2 이미지 포맷은 QEMU에서 가장 많이 쓰이는 이미지 파일 포맷이다. 이것도 하나의 파일 형태로 일정한 고정 크기의 블록 디바이스 디스크를 표현할 수 있다.
holes 을 지원하지 않는 파일 시스템에서도 qcow2는 더 적은 저장 공간을 차지할 수 있고, 게다가 선택 가능한 몇 가지 설정 항목들이 있다. 예를 들어 [AES] 암호화, zlib 압축, 스냅샷 등이다.
qcow2는 현재 비교적 주류인 가상화 이미지 포맷이다. 현재 qcow2의 성능은 raw 베어 포맷의 성능에 가깝고, 보통의 raw 포맷 이미지와 비교했을 때, 추가로 다음과 같은 특성을 가진다.
- 더 적은 저장 공간 점유, 파일 시스템이 홀(holes)을 지원하지 않더라도. qcow2 포맷의 이미지는 raw 포맷 파일보다 더 작고, 가상 머신이 실제로 디스크 공간을 점유했을 때만 그 파일이 커진다. 이는 마이그레이션에 드는 트래픽을 편리하게 줄일 수 있고, 클라우드 컴퓨팅 시나리오에 더 적합하다.
- 쓰기 시 복사(COW, copy-on-write) 지원, 이미지 파일은 베이스 디스크의 변화만 반영한다.
- 스냅샷(snapshot) 지원, 이미지 파일이 여러 개의 과거 스냅샷 이력을 포함할 수 있다.
- zlib 기반의 압축 방식 선택 가능, 각 클러스터(cluster) 단독으로 zlib 압축을 사용하도록 허용한다.
- AES 암호화 선택 가능, 128비트의 AES 키를 사용해서 암호화하는 것을 지원한다.
BlockDriver 다형성과 qcow2_open 코루틴
이전까지 bdrv_open 함수 --> bdrv_open_inherit 함수 --> bdrv_open_common 함수를 따라가면서, bdrv_open_common 함수의 두 가지 핵심 단계를 설명했다.
bdrv_open_common 함수 안에서, 디스크 파일의 포맷에 따라 BlockDriver *drv를 얻는다.
static int bdrv_open_common(BlockDriverState *bs, BlockBackend *file,
QDict *options, Error **errp)
{
int ret, open_flags;
const char *filename;
const char *driver_name = NULL;
const char *node_name = NULL;
const char *discard;
QemuOpts *opts;
BlockDriver *drv;
Error *local_err = NULL;
bool ro;
……
driver_name = qemu_opt_get(opts, "driver");
drv = bdrv_find_format(driver_name);
assert(drv != NULL);
……
}
그리고 나서 bdrv_open_driver 함수를 호출하는데, 얻어진 BlockDriver *drv에 따라 디스크 파일을 연다.
assert(!drv->bdrv_file_open || file == NULL);
ret = bdrv_open_driver(bs, drv, node_name, options, open_flags, errp);
if (ret < 0) {
goto fail_opts;
}
QEMU가 지원하는 몇 가지 디스크 파일 포맷도 확인했는데, raw, qcow2, vmdk, vdi 등이 있다.
서로 다른 포맷에 대해, 여는 방식이 같지 않다. raw와 qcow2를 예로 들면, 각각의 BlockDriver 정의는 아래와 같다.
BlockDriver bdrv_raw = {
.format_name = "raw",
.instance_size = sizeof(BDRVRawState),
.supports_zoned_children = true,
.bdrv_probe = &raw_probe,
.bdrv_reopen_prepare = &raw_reopen_prepare,
.bdrv_reopen_commit = &raw_reopen_commit,
.bdrv_reopen_abort = &raw_reopen_abort,
.bdrv_open = &raw_open,
.bdrv_child_perm = raw_child_perm,
.bdrv_co_create_opts = &raw_co_create_opts,
.bdrv_co_preadv = &raw_co_preadv,
.bdrv_co_pwritev = &raw_co_pwritev,
.bdrv_co_pwrite_zeroes = &raw_co_pwrite_zeroes,
.bdrv_co_pdiscard = &raw_co_pdiscard,
.bdrv_co_zone_report = &raw_co_zone_report,
.bdrv_co_zone_mgmt = &raw_co_zone_mgmt,
.bdrv_co_zone_append = &raw_co_zone_append,
.bdrv_co_block_status = &raw_co_block_status,
.bdrv_co_copy_range_from = &raw_co_copy_range_from,
.bdrv_co_copy_range_to = &raw_co_copy_range_to,
.bdrv_co_truncate = &raw_co_truncate,
.bdrv_co_getlength = &raw_co_getlength,
.is_format = true,
.bdrv_measure = &raw_measure,
.bdrv_co_get_info = &raw_co_get_info,
.bdrv_refresh_limits = &raw_refresh_limits,
.bdrv_probe_blocksizes = &raw_probe_blocksizes,
.bdrv_probe_geometry = &raw_probe_geometry,
.bdrv_co_eject = &raw_co_eject,
.bdrv_co_lock_medium = &raw_co_lock_medium,
.bdrv_co_ioctl = &raw_co_ioctl,
.create_opts = &raw_create_opts,
.bdrv_has_zero_init = &raw_has_zero_init,
.strong_runtime_opts = raw_strong_runtime_opts,
.mutable_opts = mutable_opts,
.bdrv_cancel_in_flight = raw_cancel_in_flight,
};
static void bdrv_raw_init(void)
{
bdrv_register(&bdrv_raw);
}
block_init(bdrv_raw_init);
BlockDriver bdrv_qcow2 = {
.format_name = "qcow2",
.instance_size = sizeof(BDRVQcow2State),
.bdrv_probe = qcow2_probe,
.bdrv_open = qcow2_open,
.bdrv_close = qcow2_close,
.bdrv_reopen_prepare = qcow2_reopen_prepare,
.bdrv_reopen_commit = qcow2_reopen_commit,
.bdrv_reopen_commit_post = qcow2_reopen_commit_post,
.bdrv_reopen_abort = qcow2_reopen_abort,
.bdrv_join_options = qcow2_join_options,
.bdrv_child_perm = bdrv_default_perms,
.bdrv_co_create_opts = qcow2_co_create_opts,
.bdrv_co_create = qcow2_co_create,
.bdrv_has_zero_init = qcow2_has_zero_init,
.bdrv_co_block_status = qcow2_co_block_status,
.bdrv_co_preadv_part = qcow2_co_preadv_part,
.bdrv_co_pwritev_part = qcow2_co_pwritev_part,
.bdrv_co_flush_to_os = qcow2_co_flush_to_os,
.bdrv_co_pwrite_zeroes = qcow2_co_pwrite_zeroes,
.bdrv_co_pdiscard = qcow2_co_pdiscard,
.bdrv_co_copy_range_from = qcow2_co_copy_range_from,
.bdrv_co_copy_range_to = qcow2_co_copy_range_to,
.bdrv_co_truncate = qcow2_co_truncate,
.bdrv_co_pwritev_compressed_part = qcow2_co_pwritev_compressed_part,
.bdrv_make_empty = qcow2_make_empty,
.bdrv_snapshot_create = qcow2_snapshot_create,
.bdrv_snapshot_goto = qcow2_snapshot_goto,
.bdrv_snapshot_delete = qcow2_snapshot_delete,
.bdrv_snapshot_list = qcow2_snapshot_list,
.bdrv_snapshot_load_tmp = qcow2_snapshot_load_tmp,
.bdrv_measure = qcow2_measure,
.bdrv_co_get_info = qcow2_co_get_info,
.bdrv_get_specific_info = qcow2_get_specific_info,
.bdrv_co_save_vmstate = qcow2_co_save_vmstate,
.bdrv_co_load_vmstate = qcow2_co_load_vmstate,
.is_format = true,
.supports_backing = true,
.bdrv_change_backing_file = qcow2_change_backing_file,
.bdrv_refresh_limits = qcow2_refresh_limits,
.bdrv_co_invalidate_cache = qcow2_co_invalidate_cache,
.bdrv_inactivate = qcow2_inactivate,
.create_opts = &qcow2_create_opts,
.amend_opts = &qcow2_amend_opts,
.strong_runtime_opts = qcow2_strong_runtime_opts,
.mutable_opts = mutable_opts,
.bdrv_co_check = qcow2_co_check,
.bdrv_amend_options = qcow2_amend_options,
.bdrv_co_amend = qcow2_co_amend,
.bdrv_detach_aio_context = qcow2_detach_aio_context,
.bdrv_attach_aio_context = qcow2_attach_aio_context,
.bdrv_supports_persistent_dirty_bitmap =
qcow2_supports_persistent_dirty_bitmap,
.bdrv_co_can_store_new_dirty_bitmap = qcow2_co_can_store_new_dirty_bitmap,
.bdrv_co_remove_persistent_dirty_bitmap =
qcow2_co_remove_persistent_dirty_bitmap,
};
static void bdrv_qcow2_init(void)
{
bdrv_register(&bdrv_qcow2);
}
block_init(bdrv_qcow2_init);
그러면, raw 포맷의 경우는 drv->bdrv_open()이 호출하는 게 바로 raw_open 함수다. qcow2 포맷의 경우는 drv->bdrv_open()이 호출하는 게 qcow2_open 함수다.
BlockDriver bdrv_raw = {
.format_name = "raw",
.instance_size = sizeof(BDRVRawState),
.supports_zoned_children = true,
.bdrv_probe = &raw_probe,
.bdrv_reopen_prepare = &raw_reopen_prepare,
.bdrv_reopen_commit = &raw_reopen_commit,
.bdrv_reopen_abort = &raw_reopen_abort,
.bdrv_open = &raw_open,
……
};
BlockDriver bdrv_qcow2 = {
.format_name = "qcow2",
.instance_size = sizeof(BDRVQcow2State),
.bdrv_probe = qcow2_probe,
.bdrv_open = qcow2_open,
.bdrv_close = qcow2_close,
……
};
BlockDriver의 함수 포인터 테이블 구조를 그림으로 정리하면 이렇다. 같은 BlockDriver 구조체 모양에 raw용 함수 한 세트, qcow2용 함수 한 세트가 들어 있는 것이다 (C 언어로 만든 다형성)

이번엔 먼저 qcow2 포맷에 대응되는 qcow2_open 함수를 살펴보자.
static int qcow2_open(BlockDriverState *bs, QDict *options, int flags,
Error **errp)
{
BDRVQcow2State *s = bs->opaque;
QCow2OpenCo qoc = {
.bs = bs,
.options = options,
.flags = flags,
.errp = errp,
.ret = -EINPROGRESS
};
int ret;
ret = bdrv_open_file_child(NULL, options, "file", bs, errp);
if (ret < 0) {
return ret;
}
/* Initialise locks */
qemu_co_mutex_init(&s->lock);
assert(!qemu_in_coroutine());
assert(qemu_get_current_aio_context() == qemu_get_aio_context());
aio_co_enter(bdrv_get_aio_context(bs),
qemu_coroutine_create(qcow2_open_entry, &qoc));
AIO_WAIT_WHILE_UNLOCKED(NULL, qoc.ret == -EINPROGRESS);
return qoc.ret;
}
qcow2_open 함수 안에서는, qemu_coroutine_create()와 aio_co_center()를 통해서 코루틴(coroutine) 하나로 들어간다.
코루틴은 일종의 경량급 스레드다. 사용자 모드 스레드라고 부르기도 한다. 전통적인 스레드의 스케줄링은 커널이 완성하지만, 코루틴은 사용자 계층에서 자주적으로 스케줄링이 가능하다. 코루틴의 장점은 다음과 같다
- 컨텍스트 전환 효율이 높다. 커널 입장에서는 전환을 감지하지 못하므로, 전환 오버헤드는 기본적으로 함수 호출과 별 차이가 없다.
- 코루틴 간 공유 데이터에 락이 필요 없다. 왜냐하면 모두 하나의 스레드 안에서 돌고 있고, 진정한 병행 실행은 아니기 때문이다.
스레드는 커널의 개념이고, 만들어진 스레드 하나하나가 커널 안에서 각각 하나의 task_struct 객체에 대응된다.
커널의 스케줄링도 스레드를 단위로 한다. 보통의 프로세스 개발에서는 이게 문제가 없지만, 여기 QEMU의 경우 가상 머신은 사용자 모드와 커널 모드 사이를 자주 왔다 갔다 하면 대가가 비교적 크다(특히 그게 가상 머신의 상태와 관련되어 있을 때).
하지만, QEMU 안의 디바이스도 동시성 능력이 필요하다. 그럼 어떻게 해야 하나? 사용자 모드에서 스레드 비슷한 메커니즘 을 구현해야 하니, 즉 코루틴을 도입해서 동시성 실현에 쓰고, 게다가 커널로 전환할 필요 없이 스케줄링을 모두 사용자 모드에서 완성한다.

여기서, qcow2 포맷의 파일 하나를 여는 데 코루틴 하나가 쓰인다. 코루틴 하나를 만드는 건 스레드 하나를 만드는 것과 비슷하다. 마찬가지로 함수 하나(코루틴 함수)를 지정해서 실행되도록 해야 하고, 위 코드 안의 qcow2_open_entry가 바로 코루틴 함수다.
qcow2_open 안의 호출 흐름

qcow2_open_entry 함수도 block/qcow2.c 안에 있고, 코드는 아래와 같다
static void coroutine_fn qcow2_open_entry(void *opaque)
{
QCow2OpenCo *qoc = opaque;
BDRVQcow2State *s = qoc->bs->opaque;
GRAPH_RDLOCK_GUARD();
qemu_co_mutex_lock(&s->lock);
qoc->ret = qcow2_do_open(qoc->bs, qoc->options, qoc->flags, true,
qoc->errp);
qemu_co_mutex_unlock(&s->lock);
aio_wait_kick();
}
함수명 앞에 붙어 있는 coroutine_fn은 하나의 코루틴 함수임을 설명해준다. 핵심은 qcow2_do_open 함수를 호출한 것이다.
qcow2_do_open 함수도 block/qcow2.c 안에 있는데, 코드가 비교적 길다(거의 600줄에 가깝다). 여기서는 깊이 파고들어서 분석하지는 않겠다. 그 주요 기능이 qcow2 포맷에 근거해서 디스크 파일을 여는 것이라는 점만 알면 된다
.
이로써, qcow2 포맷에 대응되는 qcow2_open 함수는 기본적으로 분석이 끝났다.
raw 포맷: raw_open 분석
이전까지 qcow2 포맷에 대응하는 qcow2_open 함수를 분석했는데, 이번에는 raw 포맷에 대응하는 raw_open 함수를 분석한다.
raw_open 함수는 block/raw-format.c 안에 있고, 코드는 아래와 같다.
static int raw_open(BlockDriverState *bs, QDict *options, int flags,
Error **errp)
{
BDRVRawState *s = bs->opaque;
AioContext *ctx;
bool has_size;
uint64_t offset, size;
BdrvChildRole file_role;
int ret;
ret = raw_read_options(options, &offset, &has_size, &size, errp);
if (ret < 0) {
return ret;
}
/*
* Without offset and a size limit, this driver behaves very much
* like a filter. With any such limit, it does not.
*/
if (offset || has_size) {
file_role = BDRV_CHILD_DATA | BDRV_CHILD_PRIMARY;
} else {
file_role = BDRV_CHILD_FILTERED | BDRV_CHILD_PRIMARY;
}
bdrv_open_child(NULL, options, "file", bs, &child_of_bds,
file_role, false, errp);
if (!bs->file) {
return -EINVAL;
}
bs->sg = bdrv_is_sg(bs->file->bs);
bs->supported_write_flags = BDRV_REQ_WRITE_UNCHANGED |
(BDRV_REQ_FUA & bs->file->bs->supported_write_flags);
bs->supported_zero_flags = BDRV_REQ_WRITE_UNCHANGED |
((BDRV_REQ_FUA | BDRV_REQ_MAY_UNMAP | BDRV_REQ_NO_FALLBACK) &
bs->file->bs->supported_zero_flags);
bs->supported_truncate_flags = bs->file->bs->supported_truncate_flags &
BDRV_REQ_ZERO_WRITE;
if (bs->probed && !bdrv_is_read_only(bs)) {
bdrv_refresh_filename(bs->file->bs);
fprintf(stderr,
"WARNING: Image format was not specified for '%s' and probing "
"guessed raw.\n"
" Automatically detecting the format is dangerous for "
"raw images, write operations on block 0 will be restricted.\n"
" Specify the 'raw' format explicitly to remove the "
"restrictions.\n",
bs->file->bs->filename);
}
ctx = bdrv_get_aio_context(bs);
aio_context_acquire(ctx);
ret = raw_apply_options(bs, s, offset, has_size, size, errp);
aio_context_release(ctx);
if (ret < 0) {
return ret;
}
if (bdrv_is_sg(bs) && (s->offset || s->has_size)) {
error_setg(errp, "Cannot use offset/size with SCSI generic devices");
return -EINVAL;
}
return 0;
}
raw_open 함수는 qcow2_open 함수와 메커니즘이 다른데, 그 안에 coroutine이 없다.
핵심 코드 부분은 다음과 같다.
bdrv_open_child(NULL, options, "file", bs, &child_of_bds,
file_role, false, errp);
if (!bs->file) {
return -EINVAL;
}
bdrv_open_child 함수는 block.c 안에 있고, 코드는 아래와 같다.
/*
* Opens a disk image whose options are given as BlockdevRef in another block
* device's options.
*
* If allow_none is true, no image will be opened if filename is false and no
* BlockdevRef is given. NULL will be returned, but errp remains unset.
*
* bdrev_key specifies the key for the image's BlockdevRef in the options QDict.
* That QDict has to be flattened; therefore, if the BlockdevRef is a QDict
* itself, all options starting with "${bdref_key}." are considered part of the
* BlockdevRef.
*
* The BlockdevRef will be removed from the options QDict.
*
* The caller must hold the lock of the main AioContext and no other AioContext.
* @parent can move to a different AioContext in this function. Callers must
* make sure that their AioContext locking is still correct after this.
*/
BdrvChild *bdrv_open_child(const char *filename,
QDict *options, const char *bdref_key,
BlockDriverState *parent,
const BdrvChildClass *child_class,
BdrvChildRole child_role,
bool allow_none, Error **errp)
{
BlockDriverState *bs;
BdrvChild *child;
AioContext *ctx;
GLOBAL_STATE_CODE();
bs = bdrv_open_child_bs(filename, options, bdref_key, parent, child_class,
child_role, allow_none, errp);
if (bs == NULL) {
return NULL;
}
ctx = bdrv_get_aio_context(bs);
aio_context_acquire(ctx);
child = bdrv_attach_child(parent, bs, bdref_key, child_class, child_role,
errp);
aio_context_release(ctx);
return child;
}
함수의 주석을 자세히 읽어보자.
함수 기능: 디스크 이미지를 하나 여는데, 그 옵션은 또 다른 블록 디바이스의 옵션 중 BlockdevRef로 주어진다.
파라미터 설명:
- bool allow_none
allow_none이 true라면, filename이 false이고 BlockdevRef도 주어지지 않았을 때 어떠한 이미지도 열리지 않는다. NULL을 반환하지만, errp는 여전히 설정되지 않는다.
- const char *bdref_key
bdrev_key는 옵션 QDict 안에서 이미지의 BlockdevRef를 지정하는 키다. QDict는 반드시 평탄화되어야 한다. 따라서 BlockdevRef가 QDict 자체라면, "${bdref_key}."로 시작하는 모든 옵션은 BlockdevRef의 일부로 간주된다. BlockdevRef는 옵션 QDict에서 제거된다. 호출자는 반드시 주 AioContext의 락을 보유해야 하며, 다른 AioContext의 락을 보유해서는 안 된다. 부모 노드는 이 함수 안에서 다른 AioContext로 이동할 수 있다. 호출자는 반드시 이 호출 이후 그들의 AioContext 락이 여전히 정확한지 확인해야 한다.
여기서 한마디 끼워넣자면, 사실 이전의 qcow2_open 함수에서 호출되는 bdrv_open_file_child 함수는 바로 bdrv_open_child 함수에 한 겹의 래핑을 더한 것이다.
bdrv_open_child_bs 함수도 block.c 안에 있고, 코드는 아래와 같다.
static BlockDriverState *
bdrv_open_child_bs(const char *filename, QDict *options, const char *bdref_key,
BlockDriverState *parent, const BdrvChildClass *child_class,
BdrvChildRole child_role, bool allow_none, Error **errp)
{
BlockDriverState *bs = NULL;
QDict *image_options;
char *bdref_key_dot;
const char *reference;
assert(child_class != NULL);
bdref_key_dot = g_strdup_printf("%s.", bdref_key);
qdict_extract_subqdict(options, &image_options, bdref_key_dot);
g_free(bdref_key_dot);
/*
* Caution: while qdict_get_try_str() is fine, getting non-string
* types would require more care. When @options come from
* -blockdev or blockdev_add, its members are typed according to
* the QAPI schema, but when they come from -drive, they're all
* QString.
*/
reference = qdict_get_try_str(options, bdref_key);
if (!filename && !reference && !qdict_size(image_options)) {
if (!allow_none) {
error_setg(errp,
"A block device must be specified for \"%s\"",
bdref_key);
}
qobject_unref(image_options);
goto done;
}
bs = bdrv_open_inherit(filename, reference, image_options, 0,
parent, child_class, child_role, errp);
if (!bs) {
goto done;
}
done:
qdict_del(options, bdref_key);
return bs;
}
bdrv_open_child_bs 함수 안에서는 또 이전에 다뤘던 bdrv_open_inherit 함수가 호출됐다. 이건 거의 순환처럼 보이는데, 현재 인식 수준으로는 아직 종료 조건이 명확하게 정리되지 않았다.
전체 흐름과 데이터 구조
QEMU 명령행이 입력된 순간부터 qcow2_do_open이 실제로 호출되기까지의 흐름이다

그리고 마지막으로, 이 과정에서 만들어지는 객체들이 메모리상에서 어떻게 연결되는지

이렇게 명령행의 -drive file=...,format=qcow2가 결국 BlockBackend → BlockDriverState (포맷 레이어, qcow2) → BlockDriverState (프로토콜 레이어, file) → 호스트 파일로 이어지는 트리 구조를 만들어낸다.
QEMU 시작 시 블록 디바이스 가상화의 전체 흐름

- QEMU의 시작 명령줄에서 스토리지 관련 파라미터를 설정한다 (-drive、-device 등).
- qemu_create_early_backends 함수(qemu_init 함수 안에서 호출됨) 안에서 configure_blockdev 함수를 호출해 QEMU 명령줄의 시작 파라미터 -drive를 분석하고, 초기화한다. 이 디바이스는 drive_init_func 함수로 초기화하라고 한다.
- drive_init_func 함수 안에서는 drive_new 함수를 호출해 디바이스 하나를 생성한다.
- driver_new 함수 안에서는 또 QEMU의 시작 파라미터 -device를 분석하고, driver를 virblk-blk-pci로 설정한다. 또한 file 파라미터도 분석한다.
- drive_new 함수는 이어서 blockdev_init 함수를 호출해 파라미터에 따라 초기화를 진행한다.
- blockdev_init 함수는 blk_new_open 함수를 호출, 호스트 머신상의 디스크 파일을 열고, BlockBackend를 반환한다. 이게 바로 virtio 백엔드다.
- blk_new_open 함수는 호출 체인 bdrv_open() → bdrv_open_inherit() → bdrv_open_common()을 통해, 디스크 파일의 포맷에 따라 BlockDriver를 얻는다. 다른 포맷에 대해서, 그 파일을 여는 방식이 다르다. 예를 들어 qcow2 포맷에 대해서는, 파일을 여는 함수가 qcow2_open 함수다. 반면 raw 포맷에 대해서는, 파일을 여는 함수가 raw_open 함수다.
- drive_new 함수는 마지막으로 DriverInfo를 하나 생성해서 열린 디바이스를 관리한다.
다음 회차부터는, 가상 머신의 한 프로세스가 파일 하나를 쓰는 전체 과정에 대해 분석을 진행한다.
'System Programming > Hypervisor' 카테고리의 다른 글
| QEMU 코드 분석 #8 VirtIO 디바이스 초기화— 게스트 커널 드라이버 로딩 & virtqueue 설정 (0) | 2026.03.09 |
|---|---|
| QEMU 코드 분석 #7 VirtIO 디바이스 초기화 — QEMU 백엔드 realize & device_plugged (0) | 2026.03.05 |
| QEMU 코드 분석 #6 VirtIO 기초 & 데이터 구조 (0) | 2025.02.14 |
| QEMU 코드 분석 #5 PCI 디바이스 에뮬레이션 & 인터럽트 (0) | 2024.11.18 |
| QEMU 코드 분석 #4 Machine & i440fx (0) | 2024.09.19 |