게스트 커널에서 virtio 드라이버가 로딩·초기화되는 과정과 virtqueue 생성
1) 게스트 커널의 virtio 드라이버 로딩 (디바이스 리셋 ~ DRIVER_OK)
2) virtio_pci_probe (pci_enable_device → legacy/modern → register_virtio_device)
3) virtqueue 설정 (init_vqs · find_vqs · setup_vq · vring_create_virtqueue)
게스트 커널의 virtio 드라이버 로딩
virtio 드라이버 로딩 과정
일반적으로 virtio 드라이버가 디바이스 하나를 초기화하는 과정은 다음과 같다.

(1) 디바이스 리셋
이건 지난번에 설명한 register_virtio_device 함수에서 dev->config->reset 호출로 완료된다.
여기서 주의할 점은, 옛날 버전에서 호출하던 게 dev->config->reset()이었는데, 새 버전에서는 직접 virtio_reset_device 함수를 쓴다는 거다.
/**
* virtio_reset_device - quiesce device for removal
* @dev: the device to reset
*
* Prevents device from sending interrupts and accessing memory.
*
* Generally used for cleanup during driver / device removal.
*
* Once this has been invoked, caller must ensure that
* virtqueue_notify / virtqueue_are not in progress.
*
* Note: this guarantees that vq callbacks are not in progress, however caller
* is responsible for preventing access from other contexts, such as a system
* call/workqueue/bh. Invoking virtio_break_device then flushing any such
* contexts is one way to handle that.
*/
void virtio_reset_device(struct virtio_device *dev)
{
#ifdef CONFIG_VIRTIO_HARDEN_NOTIFICATION
/*
* The below virtio_synchronize_cbs() guarantees that any
* interrupt for this line arriving after
* virtio_synchronize_vqs() has completed is guaranteed to see
* vq->broken as true.
*/
virtio_break_device(dev);
virtio_synchronize_cbs(dev);
#endif
dev->config->reset(dev);
}
EXPORT_SYMBOL_GPL(virtio_reset_device);
보다시피, 실제로 virtio_reset_device 함수는 dev->config->reset()을 간단히 감싼 거라고 할 수 있다. 단지 몇 가지 추가 설정 옵션이랑 그 옵션들에 대한 관련 처리만 더 있을 뿐이다.
(2) ACKNOWLEDGE 상태비트 설정
ACKNOWLEDGE 상태비트를 설정한다는 건, virtio 드라이버가 이미 그 디바이스를 알고 있다는 걸 나타낸다.
이것도 마찬가지로 register_virtio_device 함수에서 virtio_add_status() 함수를 통해 완료된다.
/* We always start by resetting the device, in case a previous
* driver messed it up. This also tests that code path a little. */
virtio_reset_device(dev);
/* Acknowledge that we've seen the device. */
virtio_add_status(dev, VIRTIO_CONFIG_S_ACKNOWLEDGE);
void virtio_add_status(struct virtio_device *dev, unsigned int status)
{
might_sleep();
dev->config->set_status(dev, dev->config->get_status(dev) | status);
}
EXPORT_SYMBOL_GPL(virtio_add_status);
(3) DRIVER 상태비트 설정
DRIVER 상태비트를 설정한다는 건, virtio 드라이버가 그 디바이스를 어떻게 구동할지 알고 있다는 걸 나타낸다.
이건 virtio 버스의 probe 함수인 virtio_dev_probe에서 virtio_add_status 함수를 호출해 완료한다.
static struct bus_type virtio_bus = {
.name = "virtio",
.match = virtio_dev_match,
.dev_groups = virtio_dev_groups,
.uevent = virtio_uevent,
.probe = virtio_dev_probe,
.remove = virtio_dev_remove,
};
int register_virtio_driver(struct virtio_driver *driver)
{
/* Catch this early. */
BUG_ON(driver->feature_table_size && !driver->feature_table);
driver->driver.bus = &virtio_bus;
return driver_register(&driver->driver);
}
EXPORT_SYMBOL_GPL(register_virtio_driver);
static int virtio_dev_probe(struct device *_d)
{
int err, i;
struct virtio_device *dev = dev_to_virtio(_d);
struct virtio_driver *drv = drv_to_virtio(dev->dev.driver);
u64 device_features;
u64 driver_features;
u64 driver_features_legacy;
/* We have a driver! */
virtio_add_status(dev, VIRTIO_CONFIG_S_DRIVER);
/* Figure out what features the device supports. */
device_features = dev->config->get_features(dev);
/* Figure out what features the driver supports. */
driver_features = 0;
for (i = 0; i < drv->feature_table_size; i++) {
unsigned int f = drv->feature_table[i];
BUG_ON(f >= 64);
driver_features |= (1ULL << f);
}
/* Some drivers have a separate feature table for virtio v1.0 */
if (drv->feature_table_legacy) {
driver_features_legacy = 0;
for (i = 0; i < drv->feature_table_size_legacy; i++) {
unsigned int f = drv->feature_table_legacy[i];
BUG_ON(f >= 64);
driver_features_legacy |= (1ULL << f);
}
} else {
driver_features_legacy = driver_features;
}
if (device_features & (1ULL << VIRTIO_F_VERSION_1))
dev->features = driver_features & device_features;
else
dev->features = driver_features_legacy & device_features;
/* Transport features always preserved to pass to finalize_features. */
for (i = VIRTIO_TRANSPORT_F_START; i < VIRTIO_TRANSPORT_F_END; i++)
if (device_features & (1ULL << i))
__virtio_set_bit(dev, i);
err = dev->config->finalize_features(dev);
if (err)
goto err;
if (drv->validate) {
u64 features = dev->features;
err = drv->validate(dev);
if (err)
goto err;
/* Did validation change any features? Then write them again. */
if (features != dev->features) {
err = dev->config->finalize_features(dev);
if (err)
goto err;
}
}
err = virtio_features_ok(dev);
if (err)
goto err;
err = drv->probe(dev);
if (err)
goto err;
/* If probe didn't do it, mark device DRIVER_OK ourselves. */
if (!(dev->config->get_status(dev) & VIRTIO_CONFIG_S_DRIVER_OK))
virtio_device_ready(dev);
if (drv->scan)
drv->scan(dev);
virtio_config_enable(dev);
return 0;
err:
virtio_add_status(dev, VIRTIO_CONFIG_S_FAILED);
return err;
}
핵심 코드 조각은 다음과 같다.
/* We have a driver! */
virtio_add_status(dev, VIRTIO_CONFIG_S_DRIVER);
(4) virtio 디바이스의 feature 비트 읽기, 드라이버가 설정한 feature 구하기
virtio 디바이스의 feature 비트를 읽고, 드라이버가 설정한 feature를 구한다. 이것도 위쪽의 virtio_dev_probe 함수에서 완료된다.
/* Figure out what features the device supports. */
device_features = dev->config->get_features(dev);
/* Figure out what features the driver supports. */
driver_features = 0;
for (i = 0; i < drv->feature_table_size; i++) {
unsigned int f = drv->feature_table[i];
BUG_ON(f >= 64);
driver_features |= (1ULL << f);
}
/* Some drivers have a separate feature table for virtio v1.0 */
if (drv->feature_table_legacy) {
driver_features_legacy = 0;
for (i = 0; i < drv->feature_table_size_legacy; i++) {
unsigned int f = drv->feature_table_legacy[i];
BUG_ON(f >= 64);
driver_features_legacy |= (1ULL << f);
}
} else {
driver_features_legacy = driver_features;
}
feature 협상 다이어그램

(5) 둘(virtio 디바이스의 feature와 드라이버가 설정한 feature)의 부분집합 계산
둘(virtio 디바이스의 feature와 드라이버가 설정한 feature)의 부분집합을 계산한다. 이것도 위쪽의 virtio_dev_probe 함수에서 완료된다.
if (device_features & (1ULL << VIRTIO_F_VERSION_1))
dev->features = driver_features & device_features;
else
dev->features = driver_features_legacy & device_features;
(6) 디바이스에 이 부분집합 특성 쓰기
virtio 디바이스의 feature와 드라이버가 설정한 feature의 부분집합을 계산한 후, 디바이스에 이 부분집합 특성을 쓴다. 이것도 위쪽의 virtio_dev_probe 함수에서 완료된다.
/* Transport features always preserved to pass to finalize_features. */
for (i = VIRTIO_TRANSPORT_F_START; i < VIRTIO_TRANSPORT_F_END; i++)
if (device_features & (1ULL << i))
__virtio_set_bit(dev, i);
err = dev->config->finalize_features(dev);
if (err)
goto err;
driver_features와 device_features를 계산한 다음에 virtio_finalize_features를 호출한다.
(7) FEATURES_OK 특성비트 설정
FEATURES_OK 특성비트를 설정한다. 이 시점 이후로는 virtio 드라이버가 새로운 특성을 더 받아들이지 않게 된다. 이것도 위쪽의 virtio_dev_probe 함수에서 완료된다.
err = virtio_features_ok(dev);
if (err)
goto err;
이 단계는 virtio_features_ok 함수에서 virtio_add_status 함수를 호출해서 완료한다.
/* Do some validation, then set FEATURES_OK */
static int virtio_features_ok(struct virtio_device *dev)
{
unsigned int status;
might_sleep();
if (virtio_check_mem_acc_cb(dev)) {
if (!virtio_has_feature(dev, VIRTIO_F_VERSION_1)) {
dev_warn(&dev->dev,
"device must provide VIRTIO_F_VERSION_1\n");
return -ENODEV;
}
if (!virtio_has_feature(dev, VIRTIO_F_ACCESS_PLATFORM)) {
dev_warn(&dev->dev,
"device must provide VIRTIO_F_ACCESS_PLATFORM\n");
return -ENODEV;
}
}
if (!virtio_has_feature(dev, VIRTIO_F_VERSION_1))
return 0;
virtio_add_status(dev, VIRTIO_CONFIG_S_FEATURES_OK);
status = dev->config->get_status(dev);
if (!(status & VIRTIO_CONFIG_S_FEATURES_OK)) {
dev_err(&dev->dev, "virtio: device refuses features: %x\n",
status);
return -ENODEV;
}
return 0;
}
위에서 언급한 핵심 코드 조각은 다음과 같다.
virtio_add_status(dev, VIRTIO_CONFIG_S_FEATURES_OK);
(8) 디바이스의 feature 비트 다시 읽기
디바이스의 feature 비트를 다시 읽어서, VIRTIO_CONFIG_S_FEATURES_OK가 설정됐는지 확인한다.
status = dev->config->get_status(dev);
if (!(status & VIRTIO_CONFIG_S_FEATURES_OK)) {
dev_err(&dev->dev, "virtio: device refuses features: %x\n",
status);
return -ENODEV;
}
만약 설정에 성공하지 못했다면, 디바이스가 virtio 드라이버가 설정한 일부 상태를 지원하지 않는 거고, 디바이스를 쓸 수 없다는 뜻이다. 이것도 마찬가지로 virtio_features_ok 함수에서 완료된다.
(9) 디바이스 관련 초기화 작업 수행
디바이스 관련 초기화 작업을 수행한다. 디바이스의 virtqueue 발견, virtio 디바이스의 설정 공간 읽기/쓰기 등이 포함된다. 이것들은 모두 virtio_dev_probe 함수에서 드라이버의 probe 함수(즉 drv->probe(dev))를 호출해 완료한다.
err = drv->probe(dev);
if (err)
goto err;
virtio balloon의 경우에는 virtballoon_probe 함수다.
static int virtballoon_probe(struct virtio_device *vdev)
{
struct virtio_balloon *vb;
int err;
if (!vdev->config->get) {
dev_err(&vdev->dev, "%s failure: config access disabled\n",
__func__);
return -EINVAL;
}
vdev->priv = vb = kzalloc(sizeof(*vb), GFP_KERNEL);
if (!vb) {
err = -ENOMEM;
goto out;
}
INIT_WORK(&vb->update_balloon_stats_work, update_balloon_stats_func);
INIT_WORK(&vb->update_balloon_size_work, update_balloon_size_func);
spin_lock_init(&vb->stop_update_lock);
mutex_init(&vb->balloon_lock);
init_waitqueue_head(&vb->acked);
vb->vdev = vdev;
balloon_devinfo_init(&vb->vb_dev_info);
err = init_vqs(vb);
if (err)
goto out_free_vb;
#ifdef CONFIG_BALLOON_COMPACTION
vb->vb_dev_info.migratepage = virtballoon_migratepage;
#endif
if (virtio_has_feature(vdev, VIRTIO_BALLOON_F_FREE_PAGE_HINT)) {
/*
* There is always one entry reserved for cmd id, so the ring
* size needs to be at least two to report free page hints.
*/
if (virtqueue_get_vring_size(vb->free_page_vq) < 2) {
err = -ENOSPC;
goto out_del_vqs;
}
vb->balloon_wq = alloc_workqueue("balloon-wq",
WQ_FREEZABLE | WQ_CPU_INTENSIVE, 0);
if (!vb->balloon_wq) {
err = -ENOMEM;
goto out_del_vqs;
}
INIT_WORK(&vb->report_free_page_work, report_free_page_func);
vb->cmd_id_received_cache = VIRTIO_BALLOON_CMD_ID_STOP;
vb->cmd_id_active = cpu_to_virtio32(vb->vdev,
VIRTIO_BALLOON_CMD_ID_STOP);
vb->cmd_id_stop = cpu_to_virtio32(vb->vdev,
VIRTIO_BALLOON_CMD_ID_STOP);
spin_lock_init(&vb->free_page_list_lock);
INIT_LIST_HEAD(&vb->free_page_list);
/*
* We're allowed to reuse any free pages, even if they are
* still to be processed by the host.
*/
err = virtio_balloon_register_shrinker(vb);
if (err)
goto out_del_balloon_wq;
}
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_DEFLATE_ON_OOM)) {
vb->oom_nb.notifier_call = virtio_balloon_oom_notify;
vb->oom_nb.priority = VIRTIO_BALLOON_OOM_NOTIFY_PRIORITY;
err = register_oom_notifier(&vb->oom_nb);
if (err < 0)
goto out_unregister_shrinker;
}
if (virtio_has_feature(vdev, VIRTIO_BALLOON_F_PAGE_POISON)) {
/* Start with poison val of 0 representing general init */
__u32 poison_val = 0;
/*
* Let the hypervisor know that we are expecting a
* specific value to be written back in balloon pages.
*
* If the PAGE_POISON value was larger than a byte we would
* need to byte swap poison_val here to guarantee it is
* little-endian. However for now it is a single byte so we
* can pass it as-is.
*/
if (!want_init_on_free())
memset(&poison_val, PAGE_POISON, sizeof(poison_val));
virtio_cwrite_le(vb->vdev,
struct virtio_balloon_config,
poison_val, &poison_val);
}
vb->pr_dev_info.report = virtballoon_free_page_report;
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_REPORTING)) {
unsigned int capacity;
capacity = virtqueue_get_vring_size(vb->reporting_vq);
if (capacity < PAGE_REPORTING_CAPACITY) {
err = -ENOSPC;
goto out_unregister_oom;
}
/*
* The default page reporting order is @pageblock_order, which
* corresponds to 512MB in size on ARM64 when 64KB base page
* size is used. The page reporting won't be triggered if the
* freeing page can't come up with a free area like that huge.
* So we specify the page reporting order to 5, corresponding
* to 2MB. It helps to avoid THP splitting if 4KB base page
* size is used by host.
*
* Ideally, the page reporting order is selected based on the
* host's base page size. However it needs more work to report
* that value. The hard-coded order would be fine currently.
*/
#if defined(CONFIG_ARM64) && defined(CONFIG_ARM64_64K_PAGES)
vb->pr_dev_info.order = 5;
#endif
err = page_reporting_register(&vb->pr_dev_info);
if (err)
goto out_unregister_oom;
}
virtio_device_ready(vdev);
if (towards_target(vb))
virtballoon_changed(vdev);
return 0;
out_unregister_oom:
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_DEFLATE_ON_OOM))
unregister_oom_notifier(&vb->oom_nb);
out_unregister_shrinker:
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_FREE_PAGE_HINT))
virtio_balloon_unregister_shrinker(vb);
out_del_balloon_wq:
if (virtio_has_feature(vdev, VIRTIO_BALLOON_F_FREE_PAGE_HINT))
destroy_workqueue(vb->balloon_wq);
out_del_vqs:
vdev->config->del_vqs(vdev);
out_free_vb:
kfree(vb);
out:
return err;
}
(10) DRIVER_OK 상태비트 설정
DRIVER_OK 상태비트를 설정한다. 이건 보통 구체적인 디바이스 드라이버의 probe 함수에서 virtio_device_ready 함수를 호출해 완료한다. virtio balloon 디바이스로 말하자면, 바로 위쪽의 virtballoon_probe 함수다.
virtio_device_ready(vdev);
/**
* virtio_device_ready - enable vq use in probe function
* @dev: the virtio device
*
* Driver must call this to use vqs in the probe function.
*
* Note: vqs are enabled automatically after probe returns.
*/
static inline
void virtio_device_ready(struct virtio_device *dev)
{
unsigned status = dev->config->get_status(dev);
WARN_ON(status & VIRTIO_CONFIG_S_DRIVER_OK);
#ifdef CONFIG_VIRTIO_HARDEN_NOTIFICATION
/*
* The virtio_synchronize_cbs() makes sure vring_interrupt()
* will see the driver specific setup if it sees vq->broken
* as false (even if the notifications come before DRIVER_OK).
*/
virtio_synchronize_cbs(dev);
__virtio_unbreak_device(dev);
#endif
/*
* The transport should ensure the visibility of vq->broken
* before setting DRIVER_OK. See the comments for the transport
* specific set_status() method.
*
* A well behaved device will only notify a virtqueue after
* DRIVER_OK, this means the device should "see" the coherent
* memory write that set vq->broken as false which is done by
* the driver when it sees DRIVER_OK, then the following
* driver's vring_interrupt() will see vq->broken as false so
* we won't lose any notification.
*/
dev->config->set_status(dev, status | VIRTIO_CONFIG_S_DRIVER_OK);
}
만약 디바이스 드라이버가 DRIVER_OK 상태비트를 설정하지 않았다면, 버스의 probe 함수인 virtio_dev_probe 함수에서 설정해준다. 이것도 위쪽의 virtio_dev_probe 함수에서 완료된다.
/* If probe didn't do it, mark device DRIVER_OK ourselves. */
if (!(dev->config->get_status(dev) & VIRTIO_CONFIG_S_DRIVER_OK))
virtio_device_ready(dev);
virtio 드라이버 초기화 - virtio_pci_config_ops
지금까지 virtio 드라이버의 로딩을 확인해보았다. 지금부터는 virtio 드라이버의 초기화를 확인해보자.
virtio 드라이버의 초기화를 설명하기 전에, 먼저 virtio 설정의 함수 집합 변수인 virtio_pci_config_ops를 봐야 한다.
앞에서도 언급한 적이 있는데, 아래 그림의 오른쪽 위에 있다.
virtio 관련 구조체 관계도

virtio_pci_config_ops의 초기화는 두 군데에 있다.
- drivers/virtio/virtio_pci_legacy.c
- drivers/virtio/virtio_pci_modern.c
legacy
static const struct virtio_config_ops virtio_pci_config_ops = {
.get = vp_get,
.set = vp_set,
.get_status = vp_get_status,
.set_status = vp_set_status,
.reset = vp_reset,
.find_vqs = vp_find_vqs,
.del_vqs = vp_del_vqs,
.synchronize_cbs = vp_synchronize_vectors,
.get_features = vp_get_features,
.finalize_features = vp_finalize_features,
.bus_name = vp_bus_name,
.set_vq_affinity = vp_set_vq_affinity,
.get_vq_affinity = vp_get_vq_affinity,
};
modern
static const struct virtio_config_ops virtio_pci_config_ops = {
.get = vp_get,
.set = vp_set,
.generation = vp_generation,
.get_status = vp_get_status,
.set_status = vp_set_status,
.reset = vp_reset,
.find_vqs = vp_modern_find_vqs,
.del_vqs = vp_del_vqs,
.synchronize_cbs = vp_synchronize_vectors,
.get_features = vp_get_features,
.finalize_features = vp_finalize_features,
.bus_name = vp_bus_name,
.set_vq_affinity = vp_set_vq_affinity,
.get_vq_affinity = vp_get_vq_affinity,
.get_shm_region = vp_get_shm_region,
.disable_vq_and_reset = vp_modern_disable_vq_and_reset,
.enable_vq_after_reset = vp_modern_enable_vq_after_reset,
};
여기서는 drivers/virtio/virtio_pci_modern.c의 virtio_pci_config_ops를 분석한다.
/* the PCI probing function */
int virtio_pci_modern_probe(struct virtio_device *vp_dev)
{
struct virtio_pci_modern_device *mdev = &vp_dev->mdev;
struct pci_dev *pci_dev = vp_dev->pci_dev;
int err;
mdev->pci_dev = pci_dev;
err = vp_modern_probe(mdev);
if (err)
return err;
if (mdev->device)
vp_dev->vdev.config = &virtio_pci_config_ops;
else
vp_dev->vdev.config = &virtio_pci_config_nodev_ops;
vp_dev->config_vector = vp_config_vector;
vp_dev->setup_vq = setup_vq;
vp_dev->del_vq = del_vq;
vp_dev->isr = mdev->isr;
vp_dev->vdev.id = mdev->id;
return 0;
}
virtio_pci_config_ops 변수는 virtio_device 구조체의 config 멤버에 대입된다.
/**
* struct virtio_device - representation of a device using virtio
* @index: unique position on the virtio bus
* @failed: saved value for VIRTIO_CONFIG_S_FAILED bit (for restore)
* @config_enabled: configuration change reporting enabled
* @config_change_pending: configuration change reported while disabled
* @config_lock: protects @vqs.
* @vqs_list_lock: protects @vqs.
* @dev: underlying device.
* @id: the device type identification (used to match it with a driver).
* @config: the configuration ops for this device.
* @vringh_config: configuration ops for host vrings.
* @vqs: the list of virtqueues for this device.
* @features: the features supported by both driver and device.
* @priv: private pointer for the driver's use.
*/
struct virtio_device {
int index;
bool failed;
bool config_enabled;
bool config_change_pending;
spinlock_t config_lock;
spinlock_t vqs_list_lock;
struct device dev;
struct virtio_device_id id;
const struct virtio_config_ops *config;
const struct vringh_config_ops *vringh_config;
struct list_head vqs;
u64 features;
void *priv;
};
/**
* struct virtio_config_ops - operations for configuring a virtio device
* Note: Do not assume that a transport implements all of the operations
* getting/setting a value as a simple read/write! Generally speaking,
* any of @get/@set, @get_status/@set_status, or @get_features/
* @finalize_features are NOT safe to be called from an atomic
* context.
* @get: read the value of a configuration field
* vdev: the virtio_device
* offset: the offset of the configuration field
* buf: the buffer to write the field value into.
* len: the length of the buffer
* @set: write the value of a configuration field
* vdev: the virtio_device
* offset: the offset of the configuration field
* buf: the buffer to read the field value from.
* len: the length of the buffer
* @generation: config generation counter (optional)
* vdev: the virtio_device
* Returns the config generation counter
* @get_status: read the status byte
* vdev: the virtio_device
* Returns the status byte
* @set_status: write the status byte
* vdev: the virtio_device
* status: the new status byte
* @reset: reset the device
* vdev: the virtio_device
* After this, status and feature negotiation must be done again
* Device must not be reset from its vq/config callbacks, or in
* parallel with being added/removed.
* @find_vqs: find virtqueues and instantiate them.
* vdev: the virtio_device
* nvqs: the number of virtqueues to find
* vqs: on success, includes new virtqueues
* callbacks: array of callbacks, for each virtqueue
* include a NULL entry for vqs that do not need a callback
* names: array of virtqueue names (mainly for debugging)
* include a NULL entry for vqs unused by driver
* Returns 0 on success or error status
* @del_vqs: free virtqueues found by find_vqs().
* @synchronize_cbs: synchronize with the virtqueue callbacks (optional)
* The function guarantees that all memory operations on the
* queue before it are visible to the vring_interrupt() that is
* called after it.
* vdev: the virtio_device
* @get_features: get the array of feature bits for this device.
* vdev: the virtio_device
* Returns the first 64 feature bits (all we currently need).
* @finalize_features: confirm what device features we'll be using.
* vdev: the virtio_device
* This sends the driver feature bits to the device: it can change
* the dev->feature bits if it wants.
* Note that despite the name this can be called any number of
* times.
* Returns 0 on success or error status
* @bus_name: return the bus name associated with the device (optional)
* vdev: the virtio_device
* This returns a pointer to the bus name a la pci_name from which
* the caller can then copy.
* @set_vq_affinity: set the affinity for a virtqueue (optional).
* @get_vq_affinity: get the affinity for a virtqueue (optional).
* @get_shm_region: get a shared memory region based on the index.
* @disable_vq_and_reset: reset a queue individually (optional).
* vq: the virtqueue
* Returns 0 on success or error status
* disable_vq_and_reset will guarantee that the callbacks are disabled and
* synchronized.
* Except for the callback, the caller should guarantee that the vring is
* not accessed by any functions of virtqueue.
* @enable_vq_after_reset: enable a reset queue
* vq: the virtqueue
* Returns 0 on success or error status
* If disable_vq_and_reset is set, then enable_vq_after_reset must also be
* set.
*/
struct virtio_config_ops {
void (*get)(struct virtio_device *vdev, unsigned offset,
void *buf, unsigned len);
void (*set)(struct virtio_device *vdev, unsigned offset,
const void *buf, unsigned len);
u32 (*generation)(struct virtio_device *vdev);
u8 (*get_status)(struct virtio_device *vdev);
void (*set_status)(struct virtio_device *vdev, u8 status);
void (*reset)(struct virtio_device *vdev);
int (*find_vqs)(struct virtio_device *, unsigned nvqs,
struct virtqueue *vqs[], vq_callback_t *callbacks[],
const char * const names[], const bool *ctx,
struct irq_affinity *desc);
void (*del_vqs)(struct virtio_device *);
void (*synchronize_cbs)(struct virtio_device *);
u64 (*get_features)(struct virtio_device *vdev);
int (*finalize_features)(struct virtio_device *vdev);
const char *(*bus_name)(struct virtio_device *vdev);
int (*set_vq_affinity)(struct virtqueue *vq,
const struct cpumask *cpu_mask);
const struct cpumask *(*get_vq_affinity)(struct virtio_device *vdev,
int index);
bool (*get_shm_region)(struct virtio_device *vdev,
struct virtio_shm_region *region, u8 id);
int (*disable_vq_and_reset)(struct virtqueue *vq);
int (*enable_vq_after_reset)(struct virtqueue *vq);
};
drivers/virtio/virtio_pci_modern.c의 virtio_pci_config_ops와 struct virtio_config_ops의 정의와 대조해보자
virtio_pci_config_ops 구조체의 멤버 함수들은 보통 virtio PCI proxy 디바이스의 I/O 동작이다.
virtio PCI proxy 디바이스의 PIO와 MMIO 읽기/쓰기가 포함된다.
예를 들어 get_status와 set_status 멤버에 대응되는 vp_get_status 함수와 vp_set_status 함수가 있다. 하나씩 보자.
get_status
struct virtio_config_ops의 설명에 따르면
@get_status: read the status byte
- vdev: the virtio_device
- Returns the status byte
get_status의 역할은 상태 바이트를 읽는 거다.
파라미터가 하나 있는데, vdev는 virtio device를 나타낸다. 리턴값은 읽어낸 상태 바이트다.
/* config->{get,set}_status() implementations */
static u8 vp_get_status(struct virtio_device *vdev)
{
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
return vp_modern_get_status(&vp_dev->mdev);
}
/*
* vp_modern_get_status - get the device status
* @mdev: the modern virtio-pci device
*
* Returns the status read from device
*/
u8 vp_modern_get_status(struct virtio_pci_modern_device *mdev)
{
struct virtio_pci_common_cfg __iomem *cfg = mdev->common;
return vp_ioread8(&cfg->device_status);
}
EXPORT_SYMBOL_GPL(vp_modern_get_status);
set_status
@set_status: write the status byte
- vdev: the virtio_device
- status: the new status byte
set_status의 역할은 상태 바이트를 쓰는 거다.
파라미터가 둘 있는데, vdev는 virtio device를 나타내고, status는 새로 써넣을 상태 바이트다.
static void vp_set_status(struct virtio_device *vdev, u8 status)
{
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
/* We should never be setting status to 0. */
BUG_ON(status == 0);
vp_modern_set_status(&vp_dev->mdev, status);
}
/*
* vp_modern_set_status - set status to device
* @mdev: the modern virtio-pci device
* @status: the status set to device
*/
void vp_modern_set_status(struct virtio_pci_modern_device *mdev,
u8 status)
{
struct virtio_pci_common_cfg __iomem *cfg = mdev->common;
/*
* Per memory-barriers.txt, wmb() is not needed to guarantee
* that the cache coherent memory writes have completed
* before writing to the MMIO region.
*/
vp_iowrite8(status, &cfg->device_status);
}
EXPORT_SYMBOL_GPL(vp_modern_set_status);
vp_dev->common과 QEMU의 관계
vp_modern_get_status와 vp_modern_set_status 함수는 직접 vp_dev->mdev->common->device_status를 읽고 쓴다.
이전 글에서 알 수 있듯이, vp_dev->common에 대응되는 건 virtio PCI proxy 디바이스의 네 번째 BAR가 표시하는 주소 안의 한 구간이다.
static void virtio_pci_realize(PCIDevice *pci_dev, Error **errp)
{
VirtIOPCIProxy *proxy = VIRTIO_PCI(pci_dev);
VirtioPCIClass *k = VIRTIO_PCI_GET_CLASS(pci_dev);
bool pcie_port = pci_bus_is_express(pci_get_bus(pci_dev)) &&
!pci_bus_is_root(pci_get_bus(pci_dev));
if (kvm_enabled() && !kvm_has_many_ioeventfds()) {
proxy->flags &= ~VIRTIO_PCI_FLAG_USE_IOEVENTFD;
}
/* fd-based ioevents can't be synchronized in record/replay */
if (replay_mode != REPLAY_MODE_NONE) {
proxy->flags &= ~VIRTIO_PCI_FLAG_USE_IOEVENTFD;
}
/*
* virtio pci bar layout used by default.
* subclasses can re-arrange things if needed.
*
* region 0 -- virtio legacy io bar
* region 1 -- msi-x bar
* region 2 -- virtio modern io bar (off by default)
* region 4+5 -- virtio modern memory (64bit) bar
*
*/
proxy->legacy_io_bar_idx = 0;
proxy->msix_bar_idx = 1;
proxy->modern_io_bar_idx = 2;
proxy->modern_mem_bar_idx = 4; // ← 여기가 핵심 (네 번째 BAR)
proxy->common.offset = 0x0;
proxy->common.size = 0x1000;
proxy->common.type = VIRTIO_PCI_CAP_COMMON_CFG;
PCI 설정 공간과 BAR 구조

vp_dev->mdev->common의 타입은 struct virtio_pci_common_cfg이다.
/* Fields in VIRTIO_PCI_CAP_COMMON_CFG: */
struct virtio_pci_common_cfg {
/* About the whole device. */
__le32 device_feature_select; /* read-write */
__le32 device_feature; /* read-only */
__le32 guest_feature_select; /* read-write */
__le32 guest_feature; /* read-write */
__le16 msix_config; /* read-write */
__le16 num_queues; /* read-only */
__u8 device_status; /* read-write */
__u8 config_generation; /* read-only */
/* About a specific virtqueue. */
__le16 queue_select; /* read-write */
__le16 queue_size; /* read-write, power of 2. */
__le16 queue_msix_vector; /* read-write */
__le16 queue_enable; /* read-write */
__le16 queue_notify_off; /* read-only */
__le32 queue_desc_lo; /* read-write */
__le32 queue_desc_hi; /* read-write */
__le32 queue_avail_lo; /* read-write */
__le32 queue_avail_hi; /* read-write */
__le32 queue_used_lo; /* read-write */
__le32 queue_used_hi; /* read-write */
};
struct virtio_pci_common_cfg의 각 멤버는 virtio PCI proxy 디바이스의 modern MMIO 주소 공간에서 대응되는 값을 나타낸다. 이 멤버들을 읽고 쓰면 QEMU로 trap된다.(실행흐름이 넘어간다.)
예를 들어 위의 디바이스 상태를 읽거나 설정하는 device_status 멤버는 그 주소가 virtio_pci_common_cfg 구조체 시작점에서 20바이트 위치(4+4+4+4+2+2=20)에 있다.
그래서 이 주소를 읽고 쓸 때 QEMU로 트랩되고, 그 주소는 또 virtio 디바이스의 common MemoryRegion의 20바이트 위치다.
이 MemoryRegion에 대응되는 콜백 동작 구조는 common_ops이고, 타입은 MemoryRegionOps다.
virtio_pci_common_cfg 오프셋 다이어그램

게스트 드라이버 → QEMU trap 흐름

static void virtio_pci_modern_regions_init(VirtIOPCIProxy *proxy,
const char *vdev_name)
{
static const MemoryRegionOps common_ops = {
.read = virtio_pci_common_read,
.write = virtio_pci_common_write,
.impl = {
.min_access_size = 1,
.max_access_size = 4,
},
.endianness = DEVICE_LITTLE_ENDIAN,
};
……
}
virtio_pci_config_ops의 각 함수들이 이런 I/O 동작을 캡슐화한 거다. MMIO 동작뿐만 아니라 PIO 동작도 포함된다.
virtio 디바이스는 이 구조 안의 각 콜백 함수를 통해 디바이스를 구동할 수 있다.

※ virtio_pci_device_plugged에서 게스트 드라이버 로딩으로의 전환
앞에서는 각 virtio 장치가 모두 대응하는 virtio PCI proxy 장치를 가지는걸 확인했고, 지금부터는 가상머신 내부 운영체제가 어떻게 virtio PCI proxy 장치와 virtio 장치 드라이버를 로딩하는지, 그리고 어떻게 virtio 장치와 통신하는지를 분석한다.
virtio PCI proxy 장치의 존재 때문에, PCI가 스캔을 진행할 때 이 장치를 스캔해내고, 게다가 관련된 probe 함수를 호출하게 된다.

static struct pci_driver virtio_pci_driver = {
.name = "virtio-pci",
.id_table = virtio_pci_id_table,
.probe = virtio_pci_probe,
.remove = virtio_pci_remove,
#ifdef CONFIG_PM_SLEEP
.driver.pm = &virtio_pci_pm_ops,
#endif
.sriov_configure = virtio_pci_sriov_configure,
};
module_pci_driver(virtio_pci_driver);
static int virtio_pci_probe(struct pci_dev *pci_dev,
const struct pci_device_id *id)
{
struct virtio_pci_device *vp_dev, *reg_dev = NULL;
int rc;
/* allocate our structure and fill it out */
vp_dev = kzalloc(sizeof(struct virtio_pci_device), GFP_KERNEL);
if (!vp_dev)
return -ENOMEM;
pci_set_drvdata(pci_dev, vp_dev);
vp_dev->vdev.dev.parent = &pci_dev->dev;
vp_dev->vdev.dev.release = virtio_pci_release_dev;
vp_dev->pci_dev = pci_dev;
INIT_LIST_HEAD(&vp_dev->virtqueues);
spin_lock_init(&vp_dev->lock);
/* enable the device */
rc = pci_enable_device(pci_dev);
if (rc)
goto err_enable_device;
if (force_legacy) {
rc = virtio_pci_legacy_probe(vp_dev);
/* Also try modern mode if we can't map BAR0 (no IO space). */
if (rc == -ENODEV || rc == -ENOMEM)
goto err_probe;
if (rc) {
rc = virtio_pci_modern_probe(vp_dev);
if (rc)
goto err_probe;
}
} else {
rc = virtio_pci_modern_probe(vp_dev);
if (rc == -ENODEV)
rc = virtio_pci_legacy_probe(vp_dev);
if (rc)
goto err_probe;
}
pci_set_master(pci_dev);
vp_dev->is_legacy = vp_dev->ldev.ioaddr ? true : false;
rc = register_virtio_device(&vp_dev->vdev);
reg_dev = vp_dev;
if (rc)
goto err_register;
return 0;
err_register:
if (vp_dev->is_legacy)
virtio_pci_legacy_remove(vp_dev);
else
virtio_pci_modern_remove(vp_dev);
err_probe:
pci_disable_device(pci_dev);
err_enable_device:
if (reg_dev)
put_device(&vp_dev->vdev.dev);
else
kfree(vp_dev);
return rc;
}
virtio_pci_probe 함수 전체 흐름

(1) virtio_pci_probe 함수는 virtio_pci_device 구조체 인스턴스 하나를 할당하고 vp_dev에 값을 대입한다. virtio PCI proxy 장치 하나를 표시하는 데 사용한다
struct virtio_pci_device *vp_dev, *reg_dev = NULL;
......
/* allocate our structure and fill it out */
vp_dev = kzalloc(sizeof(struct virtio_pci_device), GFP_KERNEL);
if (!vp_dev)
return -ENOMEM;
/* Our device structure */
struct virtio_pci_device {
struct virtio_device vdev;
struct pci_dev *pci_dev;
struct virtio_pci_legacy_device ldev;
struct virtio_pci_modern_device mdev;
bool is_legacy;
/* Where to read and clear interrupt */
u8 __iomem *isr;
/* a list of queues so we can dispatch IRQs */
spinlock_t lock;
struct list_head virtqueues;
/* array of all queues for house-keeping */
struct virtio_pci_vq_info **vqs;
/* MSI-X support */
int msix_enabled;
int intx_enabled;
cpumask_var_t *msix_affinity_masks;
/* Name strings for interrupts. This size should be enough,
* and I'm too lazy to allocate each name separately. */
char (*msix_names)[256];
/* Number of available vectors */
unsigned msix_vectors;
/* Vectors allocated, excluding per-vq vectors if any */
unsigned msix_used_vectors;
/* Whether we have vector per vq */
bool per_vq_vectors;
struct virtqueue *(*setup_vq)(struct virtio_pci_device *vp_dev,
struct virtio_pci_vq_info *info,
unsigned int idx,
void (*callback)(struct virtqueue *vq),
const char *name,
bool ctx,
u16 msix_vec);
void (*del_vq)(struct virtio_pci_vq_info *info);
u16 (*config_vector)(struct virtio_pci_device *vp_dev, u16 vector);
};
virtio_pci_device 구조체 구성도

(2) vp_dev를 그 pci_dev의 사적 구조에 설정한다
pci_set_drvdata(pci_dev, vp_dev);
static inline void pci_set_drvdata(struct pci_dev *pdev, void *data)
{
dev_set_drvdata(&pdev->dev, data);
}
static inline void dev_set_drvdata(struct device *dev, void *data)
{
dev->driver_data = data;
}
종합하면
pci_set_drvdata(pci_dev, vp_dev);
전개하면 실제로는 다음과 같다.
(&pci_dev->dev)->driver_data = vp_dev;
pci_set_drvdata 호출 체인

/* The pci_dev structure describes PCI devices */
struct pci_dev {
struct list_head bus_list; /* Node in per-bus list */
struct pci_bus *bus; /* Bus this device is on */
struct pci_bus *subordinate; /* Bus this device bridges to */
void *sysdata; /* Hook for sys-specific extension */
struct proc_dir_entry *procent; /* Device entry in /proc/bus/pci */
struct pci_slot *slot; /* Physical slot this device is in */
unsigned int devfn; /* Encoded device & function index */
unsigned short vendor;
unsigned short device;
unsigned short subsystem_vendor;
unsigned short subsystem_device;
unsigned int class; /* 3 bytes: (base,sub,prog-if) */
u8 revision; /* PCI revision, low byte of class word */
u8 hdr_type; /* PCI header type (`multi' flag masked out) */
#ifdef CONFIG_PCIEAER
u16 aer_cap; /* AER capability offset */
struct aer_stats *aer_stats; /* AER stats for this device */
#endif
#ifdef CONFIG_PCIEPORTBUS
struct rcec_ea *rcec_ea; /* RCEC cached endpoint association */
struct pci_dev *rcec; /* Associated RCEC device */
#endif
u32 devcap; /* PCIe Device Capabilities */
u8 pcie_cap; /* PCIe capability offset */
u8 msi_cap; /* MSI capability offset */
u8 msix_cap; /* MSI-X capability offset */
u8 pcie_mpss:3; /* PCIe Max Payload Size Supported */
u8 rom_base_reg; /* Config register controlling ROM */
u8 pin; /* Interrupt pin this device uses */
u16 pcie_flags_reg; /* Cached PCIe Capabilities Register */
unsigned long *dma_alias_mask;/* Mask of enabled devfn aliases */
struct pci_driver *driver; /* Driver bound to this device */
u64 dma_mask; /* Mask of the bits of bus address this
device implements. Normally this is
0xffffffff. You only need to change
this if your device has broken DMA
or supports 64-bit transfers. */
struct device_dma_parameters dma_parms;
pci_power_t current_state;/* Current operating state. In ACPI,
this is D0-D3, D0 being fully
functional, and D3 being off. */
unsigned int imm_ready:1; /* Supports Immediate Readiness */
u8 pm_cap; /* PM capability offset */
unsigned int pme_support:5; /* Bitmask of states from which PME#
can be generated */
unsigned int pme_poll:1; /* Poll device's PME status bit */
unsigned int d1_support:1; /* Low power state D1 is supported */
unsigned int d2_support:1; /* Low power state D2 is supported */
unsigned int no_d1d2:1; /* D1 and D2 are forbidden */
unsigned int no_d3cold:1; /* D3cold is forbidden */
unsigned int bridge_d3:1; /* Allow D3 for bridge */
unsigned int d3cold_allowed:1; /* D3cold is allowed by user */
unsigned int mmio_always_on:1; /* Disallow turning off io/mem
decoding during BAR sizing */
unsigned int wakeup_prepared:1;
unsigned int skip_bus_pm:1; /* Internal: Skip bus-level PM */
unsigned int ignore_hotplug:1; /* Ignore hotplug events */
unsigned int hotplug_user_indicators:1; /* SlotCtl
controlled exclusively by
user sysfs */
unsigned int clear_retrain_link:1; /* Need to clear Retrain Link
bit manually */
unsigned int d3hot_delay; /* D3hot->D0 transition time in ms */
unsigned int d3cold_delay; /* D3cold->D0 transition time in ms */
#ifdef CONFIG_PCIEASPM
struct pcie_link_state *link_state; /* ASPM link state */
unsigned int ltr_path:1; /* Latency Tolerance Reporting
supported from root to here */
u16 l1ss; /* L1SS Capability pointer */
#endif
unsigned int pasid_no_tlp:1; /* PASID works without TLP Prefix */
unsigned int eetlp_prefix_path:1; /* End-to-End TLP Prefix */
pci_channel_state_t error_state; /* Current connectivity state */
struct device dev; /* Generic device interface */
int cfg_size; /* Size of config space */
/*
* Instead of touching interrupt line and base address registers
* directly, use the values stored here. They might be different!
*/
unsigned int irq;
struct resource resource[DEVICE_COUNT_RESOURCE]; /* I/O and memory regions + expansion ROMs */
bool match_driver; /* Skip attaching driver */
unsigned int transparent:1; /* Subtractive decode bridge */
unsigned int io_window:1; /* Bridge has I/O window */
unsigned int pref_window:1; /* Bridge has pref mem window */
unsigned int pref_64_window:1; /* Pref mem window is 64-bit */
unsigned int multifunction:1; /* Multi-function device */
unsigned int is_busmaster:1; /* Is busmaster */
unsigned int no_msi:1; /* May not use MSI */
unsigned int no_64bit_msi:1; /* May only use 32-bit MSIs */
unsigned int block_cfg_access:1; /* Config space access blocked */
unsigned int broken_parity_status:1; /* Generates false positive parity */
unsigned int irq_reroute_variant:2; /* Needs IRQ rerouting variant */
unsigned int msi_enabled:1;
unsigned int msix_enabled:1;
unsigned int ari_enabled:1; /* ARI forwarding */
unsigned int ats_enabled:1; /* Address Translation Svc */
unsigned int pasid_enabled:1; /* Process Address Space ID */
unsigned int pri_enabled:1; /* Page Request Interface */
unsigned int is_managed:1; /* Managed via devres */
unsigned int is_msi_managed:1; /* MSI release via devres installed */
unsigned int needs_freset:1; /* Requires fundamental reset */
unsigned int state_saved:1;
unsigned int is_physfn:1;
unsigned int is_virtfn:1;
unsigned int is_hotplug_bridge:1;
unsigned int shpc_managed:1; /* SHPC owned by shpchp */
unsigned int is_thunderbolt:1; /* Thunderbolt controller */
/*
* Devices marked being untrusted are the ones that can potentially
* execute DMA attacks and similar. They are typically connected
* through external ports such as Thunderbolt but not limited to
* that. When an IOMMU is enabled they should be deemed full
* mappings to make sure they cannot access arbitrary memory.
*/
unsigned int untrusted:1;
/*
* Info from the platform, e.g., ACPI or device tree, may mark a
* device as "external-facing". An external-facing device is
* itself internal but devices downstream from it are external.
*/
unsigned int external_facing:1;
unsigned int broken_intx_masking:1; /* INTx masking can't be used */
unsigned int io_window_1k:1; /* Intel bridge 1K I/O windows */
unsigned int irq_managed:1;
unsigned int non_compliant_bars:1; /* Broken BARs; ignore them */
unsigned int is_probed:1; /* Device probing in progress */
unsigned int link_active_reporting:1;/* Device capable of reporting link active */
unsigned int no_vf_scan:1; /* Don't scan for VFs after IOV enablement */
unsigned int no_command_memory:1; /* No PCI_COMMAND_MEMORY */
unsigned int rom_bar_overlap:1; /* ROM BAR disable broken */
pci_dev_flags_t dev_flags;
atomic_t enable_cnt; /* pci_enable_device has been called */
u32 saved_config_space[16]; /* Config space saved at suspend time */
struct hlist_head saved_cap_space;
int rom_attr_enabled; /* Display of ROM attribute enabled? */
struct bin_attribute *res_attr[DEVICE_COUNT_RESOURCE]; /* sysfs file for resources */
struct bin_attribute *res_attr_wc[DEVICE_COUNT_RESOURCE]; /* sysfs file for WC mapping of resources */
#ifdef CONFIG_HOTPLUG_PCI_PCIE
unsigned int broken_cmd_compl:1; /* No compl for some cmds */
#endif
#ifdef CONFIG_PCIE_PTM
u16 ptm_cap; /* PTM Capability */
unsigned int ptm_root:1;
unsigned int ptm_enabled:1;
u8 ptm_granularity;
#endif
#ifdef CONFIG_PCI_MSI
void __iomem *msix_base;
raw_spinlock_t msi_lock;
#endif
struct pci_vpd vpd;
#ifdef CONFIG_PCIE_DPC
u16 dpc_cap;
unsigned int dpc_rp_extensions:1;
u8 dpc_rp_log_size;
#endif
#ifdef CONFIG_PCI_ATS
union {
struct pci_sriov *sriov; /* PF: SR-IOV info */
struct pci_dev *physfn; /* VF: related PF */
};
u16 ats_cap; /* ATS Capability offset */
u8 ats_stu; /* ATS Smallest Translation Unit */
#endif
#ifdef CONFIG_PCI_PRI
u16 pri_cap; /* PRI Capability offset */
u32 pri_reqs_alloc; /* Number of PRI requests allocated */
unsigned int pasid_required:1; /* PRG Response PASID Required */
#endif
#ifdef CONFIG_PCI_PASID
u16 pasid_cap; /* PASID Capability offset */
u16 pasid_features;
#endif
#ifdef CONFIG_PCI_P2PDMA
struct pci_p2pdma __rcu *p2pdma;
#endif
u16 acs_cap; /* ACS Capability offset */
phys_addr_t rom; /* Physical address if not from BAR */
size_t romlen; /* Length if not from BAR */
/*
* Driver name to force a match. Do not set directly, because core
* frees it. Use driver_set_override() to set or clear it.
*/
const char *driver_override;
unsigned long priv_flags; /* Private flags for the PCI driver */
/* These methods index pci_reset_fn_methods[] */
u8 reset_methods[PCI_NUM_RESET_METHODS]; /* In priority order */
};
(3) vp_dev 즉 virtio_pci_device 구조 안의 virtio_device 타입의 vdev 멤버의 관련 구조를 초기화한다
vp_dev->vdev.dev.parent = &pci_dev->dev;
vp_dev->vdev.dev.release = virtio_pci_release_dev;
vp_dev->pci_dev = pci_dev;
INIT_LIST_HEAD(&vp_dev->virtqueues);
spin_lock_init(&vp_dev->lock);
vp_dev 초기화 후의 관계는 다음과 같다.

전체 흐름

핵심 개념

2) virtio_pci_probe (게스트 PCI 드라이버 probe)
이제부터는 virtio_pci_probe 함수의 나머지 부분 - pci_enable_device, virtio_pci_modern_probe / virtio_pci_legacy_probe, register_virtio_device 등을 이어서 분석한다.
먼저 virtio_pci_probe 함수가 PCI 장치 하나를 받아서 virtio 장치로 등록하기까지의 큰 흐름을 보자.

static int virtio_pci_probe(struct pci_dev *pci_dev,
const struct pci_device_id *id)
{
struct virtio_pci_device *vp_dev, *reg_dev = NULL;
int rc;
/* allocate our structure and fill it out */
vp_dev = kzalloc(sizeof(struct virtio_pci_device), GFP_KERNEL);
if (!vp_dev)
return -ENOMEM;
pci_set_drvdata(pci_dev, vp_dev);
vp_dev->vdev.dev.parent = &pci_dev->dev;
vp_dev->vdev.dev.release = virtio_pci_release_dev;
vp_dev->pci_dev = pci_dev;
INIT_LIST_HEAD(&vp_dev->virtqueues);
spin_lock_init(&vp_dev->lock);
/* enable the device */
rc = pci_enable_device(pci_dev);
if (rc)
goto err_enable_device;
if (force_legacy) {
rc = virtio_pci_legacy_probe(vp_dev);
/* Also try modern mode if we can't map BAR0 (no IO space). */
if (rc == -ENODEV || rc == -ENOMEM)
rc = virtio_pci_modern_probe(vp_dev);
if (rc)
goto err_probe;
} else {
rc = virtio_pci_modern_probe(vp_dev);
if (rc == -ENODEV)
rc = virtio_pci_legacy_probe(vp_dev);
if (rc)
goto err_probe;
}
pci_set_master(pci_dev);
vp_dev->is_legacy = vp_dev->ldev.ioaddr ? true : false;
rc = register_virtio_device(&vp_dev->vdev);
reg_dev = vp_dev;
if (rc)
goto err_register;
return 0;
err_register:
if (vp_dev->is_legacy)
virtio_pci_legacy_remove(vp_dev);
else
virtio_pci_modern_remove(vp_dev);
err_probe:
pci_disable_device(pci_dev);
err_enable_device:
if (reg_dev)
put_device(&vp_dev->vdev.dev);
else
kfree(vp_dev);
return rc;
}
(4) pci_enable_device — PCI 장치 활성화
pci_enable_device 함수를 호출해서 이 PCI 장치를 활성화한다
/* enable the device */
rc = pci_enable_device(pci_dev);
if (rc)
goto err_enable_device;
pci_dev는 virtio_pci_probe 함수의 첫 번째 인자이고, 구조체 struct pci_dev 타입이다.
/**
* pci_enable_device - Initialize device before it's used by a driver.
* @dev: PCI device to be initialized
*
* Initialize device before it's used by a driver. Ask low-level code
* to enable I/O and memory. Wake up the device if it was suspended.
* Beware, this function can fail.
*
* Note we don't actually enable the device many times if we call
* this function repeatedly (we just increment the count).
*/
int pci_enable_device(struct pci_dev *dev)
{
return pci_enable_device_flags(dev, IORESOURCE_MEM | IORESOURCE_IO);
}
EXPORT_SYMBOL(pci_enable_device);
static int pci_enable_device_flags(struct pci_dev *dev, unsigned long flags)
{
struct pci_dev *bridge;
int err;
int i, bars = 0;
/*
* Power state could be unknown at this point, either due to a fresh
* boot or a device removal call. So get the current power state
* so that things like MSI message writing will behave as expected
* (e.g. if the device really is in D0 at enable time).
*/
pci_update_current_state(dev, dev->current_state);
if (atomic_inc_return(&dev->enable_cnt) > 1)
return 0; /* already enabled */
bridge = pci_upstream_bridge(dev);
if (bridge)
pci_enable_bridge(bridge);
/* only skip sriov related */
for (i = 0; i <= PCI_ROM_RESOURCE; i++)
if (dev->resource[i].flags & flags)
bars |= (1 << i);
for (i = PCI_BRIDGE_RESOURCES; i < DEVICE_COUNT_RESOURCE; i++)
if (dev->resource[i].flags & flags)
bars |= (1 << i);
err = do_pci_enable_device(dev, bars);
if (err < 0)
atomic_dec(&dev->enable_cnt);
return err;
}
(5) legacy vs modern — virtio 장치 초기화 분기
이어서 virtio_pci_legacy_probe 또는 virtio_pci_modern_probe 함수를 호출해 이 PCI 장치에 대응하는 virtio 장치를 초기화한다
if (force_legacy) {
rc = virtio_pci_legacy_probe(vp_dev);
/* Also try modern mode if we can't map BAR0 (no IO space). */
if (rc == -ENODEV || rc == -ENOMEM)
rc = virtio_pci_modern_probe(vp_dev);
if (rc)
goto err_probe;
} else {
rc = virtio_pci_modern_probe(vp_dev);
if (rc == -ENODEV)
rc = virtio_pci_legacy_probe(vp_dev);
if (rc)
goto err_probe;
}

force_legacy 플래그가 true면 먼저 virtio_pci_legacy_probe 함수를 호출하고, 에러(-ENODEV 또는 -ENOMEM)가 나면 다시 virtio_pci_modern_probe 함수를 호출한다.
force_legacy 플래그가 false면 그 반대로, 먼저 virtio_pci_modern_probe 함수를 호출하고 에러(-ENODEV 또는 -ENOMEM)가 나면 다시 virtio_pci_legacy_probe 함수를 호출한다.
force_legacy는 모듈 파라미터고, Linux 커널 /drivers/virtio/virtio_pci_common.c 안에 있다.
static bool force_legacy = false;
#if IS_ENABLED(CONFIG_VIRTIO_PCI_LEGACY)
module_param(force_legacy, bool, 0444);
MODULE_PARM_DESC(force_legacy,
"Force legacy mode for transitional virtio 1 devices");
#endif
virtio_pci_legacy_probe
/* the PCI probing function */
int virtio_pci_legacy_probe(struct virtio_pci_device *vp_dev)
{
struct virtio_pci_legacy_device *ldev = &vp_dev->ldev;
struct pci_dev *pci_dev = vp_dev->pci_dev;
int rc;
ldev->pci_dev = pci_dev;
rc = vp_legacy_probe(ldev);
if (rc)
return rc;
vp_dev->isr = ldev->isr;
vp_dev->vdev.id = ldev->id;
vp_dev->vdev.config = &virtio_pci_config_ops;
vp_dev->config_vector = vp_config_vector;
vp_dev->setup_vq = setup_vq;
vp_dev->del_vq = del_vq;
return 0;
}
virtio_pci_modern_probe
/* the PCI probing function */
int virtio_pci_modern_probe(struct virtio_pci_device *vp_dev)
{
struct virtio_pci_modern_device *mdev = &vp_dev->mdev;
struct pci_dev *pci_dev = vp_dev->pci_dev;
int err;
mdev->pci_dev = pci_dev;
err = vp_modern_probe(mdev);
if (err)
return err;
if (mdev->device)
vp_dev->vdev.config = &virtio_pci_config_ops;
else
vp_dev->vdev.config = &virtio_pci_config_nodev_ops;
vp_dev->config_vector = vp_config_vector;
vp_dev->setup_vq = setup_vq;
vp_dev->del_vq = del_vq;
vp_dev->isr = mdev->isr;
vp_dev->vdev.id = mdev->id;
return 0;
}
if (force_legacy) {
rc = virtio_pci_legacy_probe(vp_dev);
/* Also try modern mode if we can't map BAR0 (no IO space). */
if (rc == -ENODEV || rc == -ENOMEM)
rc = virtio_pci_modern_probe(vp_dev);
if (rc)
goto err_probe;
} else {
rc = virtio_pci_modern_probe(vp_dev);
if (rc == -ENODEV)
rc = virtio_pci_legacy_probe(vp_dev);
if (rc)
goto err_probe;
}
virtio_pci_modern_probe 함수 안에서 가장 핵심은 vp_modern_probe 함수를 호출하는 부분이고, 이건 Linux 커널 소스 drivers/virtio/virtio_pci_modern_dev.c 안에 있다.
/*
* vp_modern_probe: probe the modern virtio pci device, note that the
* caller is required to enable PCI device before calling this function.
* @mdev: the modern virtio-pci device
*
* Return 0 on succeed otherwise fail
*/
int vp_modern_probe(struct virtio_pci_modern_device *mdev)
{
struct pci_dev *pci_dev = mdev->pci_dev;
int err, common, isr, notify, device;
u32 notify_length;
u32 notify_offset;
check_offsets();
/* We only own devices >= 0x1000 and <= 0x107f: leave the rest. */
if (pci_dev->device < 0x1000 || pci_dev->device > 0x107f)
return -ENODEV;
if (pci_dev->device < 0x1040) {
/* Transitional devices: use the PCI subsystem device id as
* virtio device id, same as legacy driver always did.
*/
mdev->id.device = pci_dev->subsystem_device;
} else {
/* Modern devices: simply use PCI device id, but start from 0x1040. */
mdev->id.device = pci_dev->device - 0x1040;
}
mdev->id.vendor = pci_dev->subsystem_vendor;
/* check for a common config: if not, use legacy mode (bar 0). */
common = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_COMMON_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);
if (!common) {
dev_info(&pci_dev->dev,
"virtio_pci: leaving for legacy driver\n");
return -ENODEV;
}
/* If common is there, these should be too... */
isr = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_ISR_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);
notify = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_NOTIFY_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);
if (!isr || !notify) {
dev_err(&pci_dev->dev,
"virtio_pci: missing capabilities %i/%i/%i\n",
common, isr, notify);
return -EINVAL;
}
err = dma_set_mask_and_coherent(&pci_dev->dev, DMA_BIT_MASK(64));
if (err)
err = dma_set_mask_and_coherent(&pci_dev->dev,
DMA_BIT_MASK(32));
if (err)
dev_warn(&pci_dev->dev, "Failed to enable 64-bit or 32-bit DMA. Trying to continue, but this might
/* Device capability is only mandatory for devices that have
* device-specific configuration.
*/
device = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_DEVICE_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);
err = pci_request_selected_regions(pci_dev, mdev->modern_bars,
"virtio-pci-modern");
if (err)
return err;
err = -EINVAL;
mdev->common = vp_modern_map_capability(mdev, common,
sizeof(struct virtio_pci_common_cfg), 4,
0, sizeof(struct virtio_pci_common_cfg),
NULL, NULL);
if (!mdev->common)
goto err_map_common;
mdev->isr = vp_modern_map_capability(mdev, isr, sizeof(u8), 1,
0, 1,
NULL, NULL);
if (!mdev->isr)
goto err_map_isr;
/* Read notify_off_multiplier from config space. */
pci_read_config_dword(pci_dev,
notify + offsetof(struct virtio_pci_notify_cap,
notify_off_multiplier),
&mdev->notify_offset_multiplier);
/* Read notify length and offset from config space. */
pci_read_config_dword(pci_dev,
notify + offsetof(struct virtio_pci_notify_cap,
cap.length),
¬ify_length);
pci_read_config_dword(pci_dev,
notify + offsetof(struct virtio_pci_notify_cap,
cap.offset),
¬ify_offset);
/* We don't know how many VQs we'll map, ahead of the time.
* If notify length is small, map it all now.
* Otherwise, map each VQ individually later.
*/
if ((u64)notify_length + (notify_offset % PAGE_SIZE) <= PAGE_SIZE) {
mdev->notify_base = vp_modern_map_capability(mdev, notify,
2, 2,
0, notify_length,
&mdev->notify_pa);
if (!mdev->notify_base)
goto err_map_notify;
} else {
mdev->notify_map_cap = notify;
}
/* Again, we don't know how much we should map, but PAGE_SIZE
* is more than enough for all existing devices.
*/
if (device) {
mdev->device = vp_modern_map_capability(mdev, device, 0, 4,
0, PAGE_SIZE,
&mdev->device_len,
NULL);
if (!mdev->device)
goto err_map_device;
}
return 0;
err_map_device:
if (mdev->notify_base)
pci_iounmap(pci_dev, mdev->notify_base);
err_map_notify:
pci_iounmap(pci_dev, mdev->isr);
err_map_isr:
pci_iounmap(pci_dev, mdev->common);
err_map_common:
pci_release_selected_regions(pci_dev, mdev->modern_bars);
return err;
}
EXPORT_SYMBOL_GPL(vp_modern_probe);
사실 옛날 버전 KVM, 즉 Linux 커널 옛 코드에서는 vp_modern_probe 함수의 내용 대부분이 virtio_pci_modern_probe 함수 안에 그대로 박혀 있었는데, 나중에야 따로 함수로 떼낸 거다.
vp_modern_probe의 전체 단계

(1) vendor ID와 device ID 설정
vp_modern_probe는 우선 virtio 장치의 vendor ID와 device ID를 설정한다.
/* We only own devices >= 0x1000 and <= 0x107f: leave the rest. */
if (pci_dev->device < 0x1000 || pci_dev->device > 0x107f)
return -ENODEV;
if (pci_dev->device < 0x1040) {
/* Transitional devices: use the PCI subsystem device id as
* virtio device id, same as legacy driver always did.
*/
mdev->id.device = pci_dev->subsystem_device;
} else {
/* Modern devices: simply use PCI device id, but start from 0x1040. */
mdev->id.device = pci_dev->device - 0x1040;
}
mdev->id.vendor = pci_dev->subsystem_vendor;
짚고 갈 점은, virtio PCI proxy 장치의 device ID는 virtio_pci_device_plugged 함수를 다룰 때 설정한 PCI_DEVICE_ID_VIRTIO_10_BASE + VIRTIO_ID_BALLOON, 즉 0x1040+5로 잡혀 있다는 거다.
PCI_DEVICE_ID_VIRTIO_10_BASE + VIRTIO_ID_BALLOON
= 0x1040 + 5
= 0x1045
왜 이렇게 계산하냐면, virtio 장치들이 PCI device ID를 "기준값 + 장치 종류 번호" 방식으로 규칙적으로 배정받기 때문이다.
- PCI_DEVICE_ID_VIRTIO_10_BASE = 0x1040 — modern(1.0+) virtio 장치들의 공통 시작점(base). 모든 modern virtio 장치의 device ID는 이 0x1040부터 시작한다.
- VIRTIO_ID_BALLOON = 5 — balloon 장치의 고유 종류 번호. virtio 명세에서 각 장치 타입마다 번호가 정해져 있다 (net=1, block=2, ..., balloon=5 등).
그래서 "기준점 0x1040에서 balloon의 번호 5만큼 더한 0x1045"가 balloon 장치의 device ID가 된다. 다른 장치도 같은 규칙이 다.
| device | 계산 | device id |
| virtio-net | 0x1040 + 1 | 0x1041 |
| virtio-blk | 0x1040 + 2 | 0x1042 |
| virtio-balloon | 0x1040 + 5 | 0x1045 |
/*
* modern virtio-pci devices get their id assigned automatically,
* there is no need to add #defines here. It gets calculated as
*
* PCI_DEVICE_ID = PCI_DEVICE_ID_VIRTIO_10_BASE +
* virtio_bus_get_vdev_id(bus)
*/
#define PCI_DEVICE_ID_VIRTIO_10_BASE 0x1040
#define VIRTIO_ID_BALLOON 5 /* virtio balloon */

PCI Agent 장치의 설정 공간 구조
31 24 23 16 15 8 7 0
┌─────────────────────┬───────────────────────────────┐
│ Device ID │ Vendor ID │ 0x00
├─────────────────────┼───────────────────────────────┤
│ Status │ Command │ 0x04
├─────────────────────┴───────┬───────────────────────┤
│ Class Code │ Revision ID │ 0x08
├──────┬───────────┬──────────┼───────────────────────┤
│ BIST │Header Type│Latency T.│ Cache Line Size │ 0x0C
├──────┴───────────┴──────────┴───────────────────────┤
│ Base Address Register 0 │ 0x10
├─────────────────────────────────────────────────────┤
│ Base Address Register 1 │ 0x14
├─────────────────────────────────────────────────────┤
│ Base Address Register 2 │ 0x18
├─────────────────────────────────────────────────────┤
│ Base Address Register 3 │ 0x1C
├─────────────────────────────────────────────────────┤
│ Base Address Register 4 │ 0x20
├─────────────────────────────────────────────────────┤
│ Base Address Register 5 │ 0x24
├─────────────────────────────────────────────────────┤
│ Cardbus CIS Pointer │ 0x28
├─────────────────────┬───────────────────────────────┤
│ Subsystem ID │ Subsystem Vendor ID │ 0x2C
├─────────────────────┴───────────────────────────────┤
│ Expansion ROM Base Address │ 0x30
├─────────────────────────────┬───────────────────────┤
│ Reserved │ Capabilities Pointer │ 0x34
├─────────────────────────────┴───────────────────────┤
│ Reserved │ 0x38
├──────────┬──────────┬────────────┬──────────────────┤
│ MAX_Lat │ Min_Gnt │Interrupt Pin│ Interrupt Line │ 0x3C
└──────────┴──────────┴────────────┴──────────────────┘
PCI Agent 장치의 설정 공간
(2) virtio_pci_find_capability — pci capability 찾기
이어서 virtio_pci_find_capability 함수를 여러 번 호출해서 virtio PCI proxy 장치의 pci capability를 찾는다.
/* check for a common config: if not, use legacy mode (bar 0). */
common = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_COMMON_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);
if (!common) {
dev_info(&pci_dev->dev,
"virtio_pci: leaving for legacy driver\n");
return -ENODEV;
}
/* If common is there, these should be too... */
isr = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_ISR_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);
notify = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_NOTIFY_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);
if (!isr || !notify) {
dev_err(&pci_dev->dev,
"virtio_pci: missing capabilities %i/%i/%i\n",
common, isr, notify);
return -EINVAL;
}
err = dma_set_mask_and_coherent(&pci_dev->dev, DMA_BIT_MASK(64));
if (err)
err = dma_set_mask_and_coherent(&pci_dev->dev,
DMA_BIT_MASK(32));
if (err)
dev_warn(&pci_dev->dev, "Failed to enable 64-bit or 32-bit DMA. Trying to continue, but this might
/* Device capability is only mandatory for devices that have
* device-specific configuration.
*/
device = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_DEVICE_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);
이어서 virtio 장치의 레지스터 설정 정보를 PCI capability로서 설정 공간에 써넣는다.
if (modern) {
struct virtio_pci_cap cap = {
.cap_len = sizeof cap,
struct virtio_pci_notify_cap notify = {
.cap.cap_len = sizeof notify,
.notify_off_multiplier =
cpu_to_le32(virtio_pci_queue_mem_mult(proxy)),
};
struct virtio_pci_cfg_cap cfg = {
.cap.cap_len = sizeof cfg,
.cap.cfg_type = VIRTIO_PCI_CAP_PCI_CFG,
};
struct virtio_pci_notify_cap notify_pio = {
.cap.cap_len = sizeof notify,
.notify_off_multiplier = cpu_to_le32(0x0),
};
struct virtio_pci_cfg_cap *cfg_mask;
virtio_pci_modern_regions_init(proxy, vdev->name);
virtio_pci_modern_mem_region_map(proxy, &proxy->common, &cap);
virtio_pci_modern_mem_region_map(proxy, &proxy->isr, &cap);
virtio_pci_modern_mem_region_map(proxy, &proxy->device, &cap);
virtio_pci_modern_mem_region_map(proxy, &proxy->notify, ¬ify.cap);
virtio-pci는 자기 기능 수요에 맞춰서 아래와 같은 capabilities 레이아웃을 설계했다.
오른쪽이 6개의 capability이고, 그중 5개는 virtio-pci의 capability를 기술하는 용도로 쓰이고, 1개는 MSI-X의 capability를 기술하는 용도로 쓰인다.
여기서는 virtio-pci에서 쓰이는 capability만 소개한다. 다시 아래 그림을 보면, 오른쪽은 virtio-blk의 capability 레이아웃을 그리고 있고, 왼쪽은 각 capability가 가리키는 물리 주소 공간 레이아웃이다.
virtio-pci 장치의 초기화, 백엔드 알림, 데이터 전달 같은 핵심 기능은 모두 이 5개 capability에서 구현된다.
capabilities pointer ┌──┐
│98│ 0x34
└──┘
┌─────────────────────┐ ┌──────────────────┐
BAR 4 ┌──────────┐ │virtio_pci_cap_common│ │04 01 10 00 │09│ │ 0x40
│0xfe008000│ │ _cfg │ │ length 0x1000 │
└──────────┘ └─────────────────────┘ │ offset 0x00 │
└──────────────────┘
┌────────────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│struct virtio_pci_ │ │virtio_pci_cap_isr│ │04 03 10 40 │09│ │ 0x50
│ common_cfg │ │ _cfg │ │ length 0x1000 │
│ 0xfe008000 │ │ │ │ offset 0x1000 │
├────────────────────────┤ └──────────────────┘ └──────────────────┘
│ ...... │
├────────────────────────┤ ┌──────────────────┐ ┌──────────────────┐
│ isr status │ │virtio_pci_cap_ │ │04 04 10 50 │09│ │ 0x60
│ 0xfe008100 │ │ device_cfg │ │ length 0x1000 │
├────────────────────────┤ │ │ │ offset 0x2000 │
│ ...... │ └──────────────────┘ └──────────────────┘
├────────────────────────┤
│ struct virtio_pci_ │ ┌──────────────────┐ ┌──────────────────┐
│ notify_cap │ │virtio_pci_cap_ │ │04 02 14 60 │09│ │ 0x70
│ 0xfe008300 │ │ notify_cfg │ │ length 0x1000 │
└────────────────────────┘ │ │ │ offset 0x3000 │
└──────────────────┘ └──────────────────┘
┌──────────┬───┬──────────┬─────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Bits │ 0 │ 1 │ 2-31 │ │virtio_pci_cap_cfg│ │00 05 14 70 │09│ │ 0x84
├──────────┼───┼──────────┼─────────┤ │ │ │ │
│ │que│device │ │ └──────────────────┘ └──────────────────┘
│ Purpose │ue │config │reserved │
│ │int│interrupt │ │ ┌──────────────────┐
│ │err│ │ │ │ 84 11 │ 0x98
│ │upt│ │ │ └──────────────────┘
└──────────┴───┴──────────┴─────────┘
(3) PCI BAR 찾기
virtio_pci_find_capability 함수는 소속된 PCI BAR를 찾아낸 후 virt_pci_device의 modern_bars 멤버에 써 넣는다.
/* Device capability is only mandatory for devices that have
* device-specific configuration.
*/
device = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_DEVICE_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);
virtio_pci_realize 함수에서 이 modern_bars가 1<<4라는 걸 알 수 있다.
static void virtio_pci_realize(PCIDevice *pci_dev, Error **errp)
{
VirtIOPCIProxy *proxy = VIRTIO_PCI(pci_dev);
VirtioPCIClass *k = VIRTIO_PCI_GET_CLASS(pci_dev);
bool pcie_port = pci_bus_is_express(pci_get_bus(pci_dev)) &&
!pci_bus_is_root(pci_get_bus(pci_dev));
if (kvm_enabled() && !kvm_has_many_ioeventfds()) {
proxy->flags &= ~VIRTIO_PCI_FLAG_USE_IOEVENTFD;
}
/* fd-based ioevents can't be synchronized in record/replay */
if (replay_mode != REPLAY_MODE_NONE) {
proxy->flags &= ~VIRTIO_PCI_FLAG_USE_IOEVENTFD;
}
/*
* virtio pci bar layout used by default.
* subclasses can re-arrange things if needed.
*
* region 0 -- virtio legacy io bar
* region 1 -- msi-x bar
* region 2 -- virtio modern io bar (off by default)
* region 4+5 -- virtio modern memory (64bit) bar
*
*/
proxy->legacy_io_bar_idx = 0;
proxy->msix_bar_idx = 1;
proxy->modern_io_bar_idx = 2;
proxy->modern_mem_bar_idx = 4;
proxy->common.offset = 0x0;
proxy->common.size = 0x1000;
proxy->common.type = VIRTIO_PCI_CAP_COMMON_CFG;
(4) pci_request_selected_regions — BAR 주소공간 예약
이어서 pci_request_selected_regions 함수가 virtio PCI proxy 장치의 BAR 주소 공간을 예약해 둔다.
err = pci_request_selected_regions(pci_dev, mdev->modern_bars,
"virtio-pci-modern");
if (err)
return err;
(5) vp_modern_map_capability — capability를 커널 주소공간에 매핑
vp_modern_map_capability 함수를 호출해서 대응되는 capability를 PCI proxy 장치 안의 BAR 공간에서 커널 주소 공간으로 매핑한다.
err = -EINVAL;
mdev->common = vp_modern_map_capability(mdev, common,
sizeof(struct virtio_pci_common_cfg), 4,
0, sizeof(struct virtio_pci_common_cfg),
NULL, NULL);
if (!mdev->common)
goto err_map_common;
mdev->isr = vp_modern_map_capability(mdev, isr, sizeof(u8), 1,
0, 1,
NULL, NULL);
if (!mdev->isr)
goto err_map_isr;
/* Read notify_off_multiplier from config space. */
pci_read_config_dword(pci_dev,
notify + offsetof(struct virtio_pci_notify_cap,
notify_off_multiplier),
&mdev->notify_offset_multiplier);
/* Read notify length and offset from config space. */
pci_read_config_dword(pci_dev,
notify + offsetof(struct virtio_pci_notify_cap,
cap.length),
¬ify_length);
pci_read_config_dword(pci_dev,
notify + offsetof(struct virtio_pci_notify_cap,
cap.offset),
¬ify_offset);
/* We don't know how many VQs we'll map, ahead of the time.
* If notify length is small, map it all now.
* Otherwise, map each VQ individually later.
*/
if ((u64)notify_length + (notify_offset % PAGE_SIZE) <= PAGE_SIZE) {
mdev->notify_base = vp_modern_map_capability(mdev, notify,
2, 2,
0, notify_length,
&mdev->notify_pa);
if (!mdev->notify_base)
goto err_map_notify;
} else {
mdev->notify_map_cap = notify;
}
/* Again, we don't know how much we should map, but PAGE_SIZE
* is more than enough for all existing devices.
*/
if (device) {
mdev->device = vp_modern_map_capability(mdev, device, 0, 4,
0, PAGE_SIZE,
&mdev->device_len,
NULL);
if (!mdev->device)
goto err_map_device;
}
예를 들어 mp_dev(구조체 virtio_pci_modern_device *mdev = &vp_dev->mdev;)의 common 멤버는 virtio_pci_common_cfg의 데이터를 커널 주소 공간으로 매핑한 거다.
이렇게 해두면 나중에는 이 메모리 주소 공간을 통해 직접 common이라는 capability를 접근할 수 있고, 다른 capability(isr, notify, device)도 이와 비슷하게 처리된다.
PCI BAR 공간 → 커널 주소 공간 매핑

/*
* vp_modern_map_capability - map a part of virtio pci capability
* @mdev: the modern virtio-pci device
* @off: offset of the capability
* @minlen: minimal length of the capability
* @align: align requirement
* @start: start from the capability
* @size: map size
* @len: the length that is actually mapped
* @pa: physical address of the capability
*
* Returns the io address of for the part of the capability
*/
static void __iomem *
vp_modern_map_capability(struct virtio_pci_modern_device *mdev, int off,
size_t minlen, u32 align, u32 start, u32 size,
size_t *len, resource_size_t *pa)
{
struct pci_dev *dev = mdev->pci_dev;
u8 bar;
u32 offset, length;
void __iomem *p;
pci_read_config_byte(dev, off + offsetof(struct virtio_pci_cap,
bar),
&bar);
pci_read_config_dword(dev, off + offsetof(struct virtio_pci_cap, offset),
&offset);
pci_read_config_dword(dev, off + offsetof(struct virtio_pci_cap, length),
&length);
/* Check if the BAR may have changed since we requested the region. */
if (bar >= PCI_STD_NUM_BARS || !(mdev->modern_bars & (1 << bar))) {
dev_err(&dev->dev,
"virtio_pci: bar unexpectedly changed to %u\n", bar);
return NULL;
}
if (length <= start) {
dev_err(&dev->dev,
"virtio_pci: bad capability len %u (>%u expected)\n",
length, start);
return NULL;
}
if (length - start < minlen) {
dev_err(&dev->dev,
"virtio_pci: bad capability len %u (>=%zu expected)\n",
length, minlen);
return NULL;
}
length -= start;
if (start + offset < offset) {
dev_err(&dev->dev,
"virtio_pci: map wrap-around %u+%u\n",
start, offset);
return NULL;
}
offset += start;
if (offset & (align - 1)) {
dev_err(&dev->dev,
"virtio_pci: offset %u not aligned to %u\n",
offset, align);
return NULL;
}
if (length > size)
length = size;
if (len)
*len = length;
if (minlen + offset < minlen ||
minlen + offset > pci_resource_len(dev, bar)) {
dev_err(&dev->dev,
"virtio_pci: map virtio %zu@%u "
"out of range on bar %i length %lu\n",
minlen, offset,
bar, (unsigned long)pci_resource_len(dev, bar));
return NULL;
}
p = pci_iomap_range(dev, bar, offset, length);
if (!p)
dev_err(&dev->dev,
"virtio_pci: unable to map virtio %u@%u on bar %i\n",
length, offset, bar);
else if (pa)
*pa = pci_resource_start(dev, bar) + offset;
return p;
}
이렇게 하면 사실상 virtio PCI proxy 장치의 BAR가 가상 머신 커널 주소 공간에 매핑된 거고, 이후로는 이 주소들을 직접 접근하기만 하면 virtio PCI proxy 장치에 대한 설정과 제어가 가능해진다.
vp_modern_probe 함수 호출이 끝난 다음, virtio_pci_modern_probe 함수는 이어서 virtio_pci_device 안의 virtio_device의 멤버 vdev의 config 멤버를 설정한다.
device라는 capability가 있으면 virtio_pci_config_ops로 설정하고, 그렇지 않으면 virtio_pci_config_nodev_ops로 설정한다.
그 뒤로는 vpdev, 즉 struct virtio_pci_device의 몇몇 콜백 함수를 설정한다.
config_vector는 virt 큐와 MSI 인터럽트 연결과 관련된 거라 vp_config_vector로 설정하고, setup_vq는 virtio 장치의 virt queue 설정에 쓰여서 setup_vq로 설정한다. del_vq는 virt queue 삭제에 쓰여서 del_vq로 설정한다.
virtio_pci_modern_probe 함수의 마무리 단계 흐름

struct virtio_pci_device 데이터 구조 정리
/* Our device structure */
struct virtio_pci_device {
struct virtio_device vdev;
struct pci_dev *pci_dev;
union {
struct virtio_pci_legacy_device ldev;
struct virtio_pci_modern_device mdev;
};
bool is_legacy;
/* Where to read and clear interrupt */
u8 __iomem *isr;
/* a list of queues so we can dispatch IRQs */
spinlock_t lock;
struct list_head virtqueues;
/* array of all queues for house-keeping */
struct virtio_pci_vq_info **vqs;
/* MSI-X support */
int msix_enabled;
int intx_enabled;
cpumask_var_t *msix_affinity_masks;
/* Name strings for interrupts. This size should be enough,
* and I'm too lazy to allocate each name separately. */
char (*msix_names)[256];
/* Number of available vectors */
unsigned int msix_vectors;
/* Vectors allocated, excluding per-vq vectors if any */
unsigned int msix_used_vectors;
/* Whether we have vector per vq */
bool per_vq_vectors;
struct virtqueue *(*setup_vq)(struct virtio_pci_device *vp_dev,
struct virtio_pci_vq_info *info,
unsigned int idx,
void (*callback)(struct virtqueue *vq),
const char *name,
bool ctx,
u16 msix_vec);
void (*del_vq)(struct virtio_pci_vq_info *info);
u16 (*config_vector)(struct virtio_pci_device *vp_dev, u16 vector);
};
virtio_pci_modern_probe 실행이 끝난 후 관련 데이터 구조는 아래와 같이 엮인다.

register_virtio_device — virtio 장치를 시스템에 등록
다시 virtio_pci_probe 함수로 돌아가자. virtio_pci_probe 함수는 virtio_pci_modern_probe 함수 호출이 끝난 다음, 이어서 register_virtio_device를 호출한다.
rc = register_virtio_device(&vp_dev->vdev);
reg_dev = vp_dev;
if (rc)
goto err_register;
/**
* register_virtio_device - register virtio device
* @dev : virtio device to be registered
*
* On error, the caller must call put_device on &@dev->dev (and not kfree),
* as another code path may have obtained a reference to @dev.
*
* Returns: 0 on sucess, -error on failure
*/
int register_virtio_device(struct virtio_device *dev)
{
int err;
dev->dev.bus = &virtio_bus;
device_initialize(&dev->dev);
/* Assign a unique device index and hence name. */
err = ida_alloc(&virtio_index_ida, GFP_KERNEL);
if (err < 0)
goto out;
dev->index = err;
err = dev_set_name(&dev->dev, "virtio%u", dev->index);
if (err)
goto out_ida_remove;
err = virtio_device_of_init(dev);
if (err)
goto out_ida_remove;
spin_lock_init(&dev->config_lock);
dev->config_enabled = false;
dev->config_change_pending = false;
INIT_LIST_HEAD(&dev->vqs);
spin_lock_init(&dev->vqs_list_lock);
/* We always start by resetting the device, in case a previous
* driver messed it up. This also tests that code path a little. */
virtio_reset_device(dev);
/* Acknowledge that we've seen the device. */
virtio_add_status(dev, VIRTIO_CONFIG_S_ACKNOWLEDGE);
/*
* device_add() causes the bus infrastructure to look for a matching
* driver.
*/
err = device_add(&dev->dev);
if (err)
goto out_of_node_put;
return 0;
out_of_node_put:
of_node_put(dev->dev.of_node);
out_ida_remove:
ida_free(&virtio_index_ida, dev->index);
out:
virtio_add_status(dev, VIRTIO_CONFIG_S_FAILED);
return err;
}
EXPORT_SYMBOL_GPL(register_virtio_device);
앞에서 짚었듯이, vp_dev->vdev의 타입은 struct virtio_device고, register_virtio_device 함수에 실제로 넘기는 인자는 vp_dev->vdev의 주소, 즉 &vp_dev->vdev다.
함수 이름과 인자 타입만 봐도 알 수 있듯이, register_virtio_device 함수는 virtio 장치 하나를 시스템에 등록하는 역할을 한다.
register_virtio_device의 전체 단계

(1) Bus를 virtio_bus로 설정
virtio 장치의 Bus를 virtio_bus로 설정한다.
dev->dev.bus = &virtio_bus;
virtio_bus는 시스템 초기화 시점에 register_virtio_driver 함수를 통해 시스템에 등록된다.
(2) 장치 이름 설정
virtio 장치의 이름을 "virtio0", "virtio1" 같은 문자열로 설정한다.
/* Assign a unique device index and hence name. */
err = ida_alloc(&virtio_index_ida, GFP_KERNEL);
if (err < 0)
goto out;
dev->index = err;
err = dev_set_name(&dev->dev, "virtio%u", dev->index);
if (err)
goto out_ida_remove;
/**
* dev_set_name - set a device name
* @dev: device
* @fmt: format string for the device's name
*/
int dev_set_name(struct device *dev, const char *fmt, ...)
{
va_list vargs;
int err;
va_start(vargs, fmt);
err = kobject_set_name_vargs(&dev->kobj, fmt, vargs);
va_end(vargs);
return err;
}
EXPORT_SYMBOL_GPL(dev_set_name);
(3) virtio_reset_device로 장치 리셋
그다음 virtio_reset_device 함수를 호출해 장치를 리셋한다.
/* We always start by resetting the device, in case a previous
* driver messed it up. This also tests that code path a little. */
virtio_reset_device(dev);
(4) device_add로 장치를 시스템에 등록
마지막으로 device_add 함수를 호출해 장치를 시스템에 등록한다.
/*
* device_add() causes the bus infrastructure to look for a matching
* driver.
*/
err = device_add(&dev->dev);
if (err)
goto out_of_node_put;
여기서, 옛 버전 코드에서는 device_register 함수를 호출했다. device_register 함수는 장치 드라이버와 관련이 좀 더 크니까, 여기서 그 역할을 간단히 짚고 가자.
/**
* device_register - register a device with the system.
* @dev: pointer to the device structure
*
* This happens in two clean steps - initialize the device
* and add it to the system. The two steps can be called
* separately, but this is the easiest and most common.
* I.e. you should only call the two helpers separately if
* have a clearly defined need to use and refcount the device
* before it is added to the hierarchy.
*
* For more information, see the kerneldoc for device_initialize()
* and device_add().
*
* NOTE: _Never_ directly free @dev after calling this function, even
* if it returned an error! Always use put_device() to give up the
* reference initialized in this function instead.
*/
int device_register(struct device *dev)
{
device_initialize(dev);
return device_add(dev);
}
EXPORT_SYMBOL_GPL(device_register);
device_register 함수는 시스템에 장치 하나를 등록한다. 두 가지 간단한 단계로 나뉘는데
- 장치 초기화(device_initialize(dev))
- 시스템에 추가하는 단계(device_add(dev))
이 두 단계는 따로 따로 호출해도 되지만, 같이 묶어서 device_register 함수를 쓰는 게 가장 간단하고 가장 흔한 방법이다.
예를 들어, 만약 장치를 계층 구조에 추가하기 전에 사용해서 참조 카운트를 손봐야 하는 명확한 필요가 있다면, 이 두 헬퍼 함수를 각자 따로 호출해야 한다.
옛 버전 vs 새 버전 호출 방식 비교

이 부분 코드에서도 알 수 있듯이,
옛 버전 커널 코드에서는 정말로 device_register 함수를 직접 호출했지만,
새 버전 커널 코드는 여기서 register_virtio_device 함수의 앞쪽에서 먼저 device_initialize(&dev->dev)를 호출하고, 그 후 여기서 device_add(&dev->dev)를 호출한다. 즉 분리 호출 방식을 쓴 거다.
device_register 함수는 device_add 함수를 호출해서 장치를 시스템에 추가하고, 동시에 사용자 공간으로 uevent 메시지를 한 번 보낸다.
이 uevent 메시지 안에는 virtio 장치의 vendor id, device id가 담겨 있다. udev가 이 메시지를 받고 나면, virtio 장치에 대응하는 드라이버를 로드한다.
그다음, device_add 함수는 bus_probe_device 함수를 호출하고, 최종적으로 Bus의 probe 함수와 장치의 probe 함수, 즉 virtio_dev_probe 함수와 virtballoon_probe 함수를 호출하게 된다.
/**
* device_add - add device to device hierarchy.
* @dev: device.
*
* This is part 2 of device_register(), though may be called
* separately _iff_ device_initialize() has been called separately.
*
* This adds @dev to the kobject hierarchy via kobject_add(), adds it
* to the global and sibling lists for the device, then
* adds it to the other relevant subsystems of the driver model.
*
* Do not call this routine or device_register() more than once for
* any device structure. The driver model core is not designed to work
* with devices that get unregistered and then spring back to life.
* (Among other things, it's very hard to guarantee that all references
* to the previous incarnation of @dev have been dropped.) Allocate
* and register a fresh new struct device instead.
*
* NOTE: _Never_ directly free @dev after calling this function, even
* if it returned an error! Always use put_device() to give up your
* reference instead.
*
* Rule of thumb is: if device_add() succeeds, you should call
* device_del() when you want to get rid of it. If device_add() has
* *not* succeeded, use *only* put_device() to drop the reference
* count.
*/
int device_add(struct device *dev)
{
struct subsys_private *sp;
struct device *parent;
struct kobject *kobj;
struct class_interface *class_intf;
int error = -EINVAL;
struct kobject *glue_dir = NULL;
dev = get_device(dev);
if (!dev)
goto done;
if (!dev->p) {
error = device_private_init(dev);
if (error)
goto done;
}
/*
* for statically allocated devices, which should all be converted
* some day, we need to initialize the name. We prevent reading back
* the name, and force the use of dev_name()
*/
if (dev->init_name) {
error = dev_set_name(dev, "%s", dev->init_name);
dev->init_name = NULL;
}
if (dev_name(dev))
error = 0;
/* subsystems can specify simple device enumeration */
else if (dev->bus && dev->bus->dev_name)
error = dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);
else
error = -EINVAL;
if (error)
goto name_error;
pr_debug("device: '%s': %s\n", dev_name(dev), __func__);
parent = get_device(dev->parent);
kobj = get_device_parent(dev, parent);
if (IS_ERR(kobj)) {
error = PTR_ERR(kobj);
goto parent_error;
}
if (kobj)
dev->kobj.parent = kobj;
/* use parent numa_node */
if (parent && (dev_to_node(dev) == NUMA_NO_NODE))
set_dev_node(dev, dev_to_node(parent));
/* first, register with generic layer. */
/* we require the name to be set before, and pass NULL */
error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);
if (error) {
glue_dir = kobj;
goto Error;
}
/* notify platform of device entry */
device_platform_notify(dev);
error = device_create_file(dev, &dev_attr_uevent);
if (error)
goto attrError;
error = device_add_class_symlinks(dev);
if (error)
goto SymlinkError;
error = device_add_attrs(dev);
if (error)
goto AttrsError;
error = bus_add_device(dev);
if (error)
goto BusError;
error = dpm_sysfs_add(dev);
if (error)
goto DPMError;
device_pm_add(dev);
if (MAJOR(dev->devt)) {
error = device_create_file(dev, &dev_attr_dev);
if (error)
goto DevAttrError;
error = device_create_sys_dev_entry(dev);
if (error)
goto SysEntryError;
devtmpfs_create_node(dev);
}
/* Notify clients of device addition. This call must come
* after dpm_sysfs_add() and before kobject_uevent().
*/
bus_notify(dev, BUS_NOTIFY_ADD_DEVICE);
kobject_uevent(&dev->kobj, KOBJ_ADD);
/*
* Check if any of the other devices (consumers) have been waiting for
* this device (supplier) to be added so that they can create a device
* link to it.
*
* This needs to happen after device_pm_add() because device_link_add()
* requires the supplier be registered before it's called.
*
* But this also needs to happen before bus_probe_device() to make sure
* waiting consumers can link to it before the driver is bound to the
* device and the driver sync_state callback is called for this device.
*/
if (dev->fwnode && !dev->fwnode->dev) {
dev->fwnode->dev = dev;
fw_devlink_link_device(dev);
}
bus_probe_device(dev);
/*
* If all driver registration is done and a newly added device doesn't
* match with any driver, don't block its consumers from probing in
* case the consumer device is able to operate without this supplier.
*/
if (dev->fwnode && fw_devlink_drv_reg_done && !dev->fwnode->can_match)
fw_devlink_unblock_consumers(dev);
if (parent)
klist_add_tail(&dev->p->knode_parent,
&parent->p->klist_children);
sp = class_to_subsys(dev->class);
if (sp) {
mutex_lock(&sp->mutex);
/* tie the class to the device */
klist_add_tail(&dev->p->knode_class, &sp->klist_devices);
/* notify any interfaces that the device is here */
list_for_each_entry(class_intf, &sp->interfaces, node)
if (class_intf->add_dev)
class_intf->add_dev(dev);
mutex_unlock(&sp->mutex);
subsys_put(sp);
}
done:
put_device(dev);
return error;
SysEntryError:
if (MAJOR(dev->devt))
device_remove_file(dev, &dev_attr_dev);
DevAttrError:
device_pm_remove(dev);
dpm_sysfs_remove(dev);
DPMError:
dev->driver = NULL;
bus_remove_device(dev);
BusError:
device_remove_attrs(dev);
AttrsError:
device_remove_class_symlinks(dev);
SymlinkError:
device_remove_file(dev, &dev_attr_uevent);
attrError:
device_platform_notify_remove(dev);
kobject_uevent(&dev->kobj, KOBJ_REMOVE);
glue_dir = get_glue_dir(dev);
kobject_del(&dev->kobj);
Error:
cleanup_glue_dir(dev, glue_dir);
parent_error:
put_device(parent);
name_error:
kfree(dev->p);
dev->p = NULL;
goto done;
}
EXPORT_SYMBOL_GPL(device_add);
그중에서 핵심은
/* Notify clients of device addition. This call must come
* after dpm_sysfs_add() and before kobject_uevent().
*/
bus_notify(dev, BUS_NOTIFY_ADD_DEVICE);
kobject_uevent(&dev->kobj, KOBJ_ADD);
그리고
bus_probe_device(dev);
전체 호출 시퀀스 정리

여기까지가 virtio_pci_probe 함수 전체 분석이다. 정리하면

3) virtqueue 설정 (init_vqs · find_vqs · setup_vq · vring_create)
virtballoon_probe 함수 분석

이제 virtio balloon 디바이스의 초기화 과정을 예로 들어, virtio 디바이스의 초기화 과정을 분석한다. 즉 앞서 다룬 virtio 드라이버가 디바이스를 초기화하는 과정 중 "(9) 디바이스 관련 초기화 작업 수행" 단계다.
(9) 디바이스 관련 초기화 작업 수행
디바이스 관련 초기화 작업에는 디바이스의 virtqueue 발견, virtio 디바이스의 설정 공간 읽기/쓰기 등이 포함된다. 이런 작업들은 모두 virtio_dev_probe 함수에서 드라이버의 probe 함수(즉 drv->probe(dev))를 호출함으로써 완료된다.
err = drv->probe(dev);
if (err)
goto err;
virtio balloon의 경우에는 virtballoon_probe 함수다.
static int virtballoon_probe(struct virtio_device *vdev)
{
struct virtio_balloon *vb;
int err;
if (!vdev->config->get) {
dev_err(&vdev->dev, "%s failure: config access disabled\n",
__func__);
return -EINVAL;
}
vdev->priv = vb = kzalloc(sizeof(*vb), GFP_KERNEL);
if (!vb) {
err = -ENOMEM;
goto out;
}
INIT_WORK(&vb->update_balloon_stats_work, update_balloon_stats_func);
INIT_WORK(&vb->update_balloon_size_work, update_balloon_size_func);
spin_lock_init(&vb->stop_update_lock);
mutex_init(&vb->balloon_lock);
init_waitqueue_head(&vb->acked);
vb->vdev = vdev;
balloon_devinfo_init(&vb->vb_dev_info);
err = init_vqs(vb);
if (err)
goto out_free_vb;
#ifdef CONFIG_BALLOON_COMPACTION
vb->vb_dev_info.migratepage = virtballoon_migratepage;
#endif
if (virtio_has_feature(vdev, VIRTIO_BALLOON_F_FREE_PAGE_HINT)) {
/*
* There is always one entry reserved for cmd id, so the ring
* size needs to be at least two to report free page hints.
*/
if (virtqueue_get_vring_size(vb->free_page_vq) < 2) {
err = -ENOSPC;
goto out_del_vqs;
}
vb->balloon_wq = alloc_workqueue("balloon-wq",
WQ_FREEZABLE | WQ_CPU_INTENSIVE, 0);
if (!vb->balloon_wq) {
err = -ENOMEM;
goto out_del_vqs;
}
INIT_WORK(&vb->report_free_page_work, report_free_page_func);
vb->cmd_id_received_cache = VIRTIO_BALLOON_CMD_ID_STOP;
vb->cmd_id_active = cpu_to_virtio32(vb->vdev,
VIRTIO_BALLOON_CMD_ID_STOP);
vb->cmd_id_stop = cpu_to_virtio32(vb->vdev,
VIRTIO_BALLOON_CMD_ID_STOP);
spin_lock_init(&vb->free_page_list_lock);
INIT_LIST_HEAD(&vb->free_page_list);
/*
* We're allowed to reuse any free pages, even if they are
* still to be processed by the host.
*/
err = virtio_balloon_register_shrinker(vb);
if (err)
goto out_del_balloon_wq;
}
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_DEFLATE_ON_OOM)) {
vb->oom_nb.notifier_call = virtio_balloon_oom_notify;
vb->oom_nb.priority = VIRTIO_BALLOON_OOM_NOTIFY_PRIORITY;
err = register_oom_notifier(&vb->oom_nb);
if (err < 0)
goto out_unregister_shrinker;
}
if (virtio_has_feature(vdev, VIRTIO_BALLOON_F_PAGE_POISON)) {
/* Start with poison val of 0 representing general init */
__u32 poison_val = 0;
/*
* Let the hypervisor know that we are expecting a
* specific value to be written back in balloon pages.
*
* If the PAGE_POISON value was larger than a byte we would
* need to byte swap poison_val here to guarantee it is
* little-endian. However for now it is a single byte so we
* can pass it as-is.
*/
if (!want_init_on_free())
memset(&poison_val, PAGE_POISON, sizeof(poison_val));
virtio_cwrite_le(vb->vdev, struct virtio_balloon_config,
poison_val, &poison_val);
}
vb->pr_dev_info.report = virtballoon_free_page_report;
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_REPORTING)) {
unsigned int capacity;
capacity = virtqueue_get_vring_size(vb->reporting_vq);
if (capacity < PAGE_REPORTING_CAPACITY) {
err = -ENOSPC;
goto out_unregister_oom;
}
/*
* The default page reporting order is @pageblock_order, which
* corresponds to 512MB in size on ARM64 when 64KB base page
* size is used. The page reporting won't be triggered if the
* freeing page can't come up with a free area like that huge.
* So we specify the page reporting order to 5, corresponding
* to 2MB. It helps to avoid THP splitting if 4KB base page
* size is used by host.
*
* Ideally, the page reporting order is selected based on the
* host's base page size. However, it needs more work to report
* that value. The hard-coded order would be fine currently.
*/
#if defined(CONFIG_ARM64) && defined(CONFIG_ARM64_64K_PAGES)
vb->pr_dev_info.order = 5;
#endif
err = page_reporting_register(&vb->pr_dev_info);
if (err)
goto out_unregister_oom;
}
virtio_device_ready(vdev);
if (towards_target(vb))
virtballoon_changed(vdev);
return 0;
out_unregister_oom:
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_DEFLATE_ON_OOM))
unregister_oom_notifier(&vb->oom_nb);
out_unregister_shrinker:
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_FREE_PAGE_HINT))
virtio_balloon_unregister_shrinker(vb);
out_del_balloon_wq:
if (virtio_has_feature(vdev, VIRTIO_BALLOON_F_FREE_PAGE_HINT))
destroy_workqueue(vb->balloon_wq);
out_del_vqs:
vdev->config->del_vqs(vdev);
out_free_vb:
kfree(vb);
out:
return err;
}
virtio balloon 디바이스는 virtio_balloon 구조체로 표현된다.
struct virtio_balloon {
struct virtio_device *vdev;
struct virtqueue *inflate_vq, *deflate_vq, *stats_vq, *free_page_vq;
/* Balloon's own wq for cpu-intensive work items */
struct workqueue_struct *balloon_wq;
/* The free page reporting work item submitted to the balloon wq */
struct work_struct report_free_page_work;
/* The balloon servicing is delegated to a freezable workqueue. */
struct work_struct update_balloon_stats_work;
struct work_struct update_balloon_size_work;
/* Prevent updating balloon when it is being canceled. */
spinlock_t stop_update_lock;
bool stop_update;
/* Bitmap to indicate if reading the related config fields are needed */
unsigned long config_read_bitmap;
/* The list of allocated free pages, waiting to be given back to mm */
struct list_head free_page_list;
spinlock_t free_page_list_lock;
/* The number of free page blocks on the above list */
unsigned long num_free_page_blocks;
/*
* The cmd id received from host.
* Read it via virtio_balloon_cmd_id_received to get the latest value
* sent from host.
*/
u32 cmd_id_received_cache;
/* The cmd id that is actively in use */
__virtio32 cmd_id_active;
/* Buffer to store the sign stop */
__virtio32 cmd_id_stop;
/* Waiting for host to ack the pages we released. */
wait_queue_head_t acked;
/* Number of balloon pages we've told the Host we're not using. */
unsigned int num_pages;
/*
* The pages we've told the Host we're not using are enqueued
* at vb_dev_info->pages list.
* Each page on this list adds VIRTIO_BALLOON_PAGES_PER_PAGE
* to num_pages above.
*/
struct balloon_dev_info vb_dev_info;
/* Synchronize access/update to this struct virtio_balloon elements */
struct mutex balloon_lock;
/* The array of pfns we tell the Host about. */
unsigned int num_pfns;
__virtio32 pfns[VIRTIO_BALLOON_ARRAY_PFNS_MAX];
/* Memory statistics */
struct virtio_balloon_stat stats[VIRTIO_BALLOON_S_NR];
/* Shrinker to return free pages - VIRTIO_BALLOON_F_FREE_PAGE_HINT */
struct shrinker *shrinker;
/* OOM notifier to deflate on OOM - VIRTIO_BALLOON_F_DEFLATE_ON_OOM */
struct notifier_block oom_nb;
/* Free page reporting device */
struct virtqueue *reporting_vq;
struct page_reporting_dev_info pr_dev_info;
};
virtio_balloon 구조체(struct virtio_balloon) 안에는 해당 virtio balloon 디바이스와 밀접하게 관련된 데이터 멤버들이 들어있다.
(1) virtballoon_probe 함수는 먼저 virtio_balloon 구조체 객체 하나를 할당한 후 vb에 부여하고, virtio_device의 priv에도 이 구조체 객체의 주소를 저장한다.
struct virtio_balloon *vb;
……
vdev->priv = vb = kzalloc(sizeof(*vb), GFP_KERNEL);
if (!vb) {
err = -ENOMEM;
goto out;
}
(2) 이어서 할당된 virtio_balloon 객체의 멤버에 대한 초기화를 진행한다.
기본적으로 이 함수의 나머지 코드가 그것이다. 그중 두 개의 중요한 함수가 있는데, init_vqs()와 virtio_device_ready()다.
(10) DRIVER_OK 상태 비트 설정
DRIVER_OK 상태 비트 설정은 보통 구체적인 디바이스 드라이버의 probe 함수에서, virtio_device_ready 함수를 호출함으로써 완료된다.
virtio balloon 디바이스의 경우, 바로 위에서 본 virtballoon_probe 함수다.
virtio_device_ready(vdev);
앞의 함수 init_vqs는 virtqueue와 vring을 초기화하는 데 사용되며, virtio 드라이버와 virtio 디바이스는 virtqueue를 통해 데이터를 통신한다. 아래에서 init_vqs 함수를 자세히 분석한다.
virtio 전체 아키텍처

init_vqs 함수 분석
static int init_vqs(struct virtio_balloon *vb)
{
struct virtqueue *vqs[VIRTIO_BALLOON_VQ_MAX];
vq_callback_t *callbacks[VIRTIO_BALLOON_VQ_MAX];
const char *names[VIRTIO_BALLOON_VQ_MAX];
int err;
/*
* Inflateq and deflateq are used unconditionally. The names[]
* will be NULL if the related feature is not enabled, which will
* cause no allocation for the corresponding virtqueue in find_vqs.
*/
callbacks[VIRTIO_BALLOON_VQ_INFLATE] = balloon_ack;
names[VIRTIO_BALLOON_VQ_INFLATE] = "inflate";
callbacks[VIRTIO_BALLOON_VQ_DEFLATE] = balloon_ack;
names[VIRTIO_BALLOON_VQ_DEFLATE] = "deflate";
callbacks[VIRTIO_BALLOON_VQ_STATS] = NULL;
names[VIRTIO_BALLOON_VQ_STATS] = NULL;
callbacks[VIRTIO_BALLOON_VQ_FREE_PAGE] = NULL;
names[VIRTIO_BALLOON_VQ_FREE_PAGE] = NULL;
names[VIRTIO_BALLOON_VQ_REPORTING] = NULL;
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_STATS_VQ)) {
names[VIRTIO_BALLOON_VQ_STATS] = "stats";
callbacks[VIRTIO_BALLOON_VQ_STATS] = stats_request;
}
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_FREE_PAGE_HINT)) {
names[VIRTIO_BALLOON_VQ_FREE_PAGE] = "free_page_vq";
callbacks[VIRTIO_BALLOON_VQ_FREE_PAGE] = NULL;
}
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_REPORTING)) {
names[VIRTIO_BALLOON_VQ_REPORTING] = "reporting_vq";
callbacks[VIRTIO_BALLOON_VQ_REPORTING] = balloon_ack;
}
err = virtio_find_vqs(vb->vdev, VIRTIO_BALLOON_VQ_MAX, vqs,
callbacks, names, NULL);
if (err)
return err;
vb->inflate_vq = vqs[VIRTIO_BALLOON_VQ_INFLATE];
vb->deflate_vq = vqs[VIRTIO_BALLOON_VQ_DEFLATE];
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_STATS_VQ)) {
struct scatterlist sg;
unsigned int num_stats;
vb->stats_vq = vqs[VIRTIO_BALLOON_VQ_STATS];
/*
* Prime this virtqueue with one buffer so the hypervisor can
* use it to signal us later (it can't be broken yet!).
*/
num_stats = update_balloon_stats(vb);
sg_init_one(&sg, vb->stats, sizeof(vb->stats[0]) * num_stats);
err = virtqueue_add_outbuf(vb->stats_vq, &sg, 1, vb,
GFP_KERNEL);
if (err) {
dev_warn(&vb->vdev->dev, "%s: add stat_vq failed\n",
__func__);
return err;
}
virtqueue_kick(vb->stats_vq);
}
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_FREE_PAGE_HINT))
vb->free_page_vq = vqs[VIRTIO_BALLOON_VQ_FREE_PAGE];
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_REPORTING))
vb->reporting_vq = vqs[VIRTIO_BALLOON_VQ_REPORTING];
return 0;
}
(1) init_vqs 함수는 먼저 callbacks 포인터 배열과 names 포인터 배열을 초기화한다.
struct virtqueue *vqs[VIRTIO_BALLOON_VQ_MAX];
vq_callback_t *callbacks[VIRTIO_BALLOON_VQ_MAX];
const char *names[VIRTIO_BALLOON_VQ_MAX];
int err;
/*
* Inflateq and deflateq are used unconditionally. The names[]
* will be NULL if the related feature is not enabled, which will
* cause no allocation for the corresponding virtqueue in find_vqs.
*/
callbacks[VIRTIO_BALLOON_VQ_INFLATE] = balloon_ack;
names[VIRTIO_BALLOON_VQ_INFLATE] = "inflate";
callbacks[VIRTIO_BALLOON_VQ_DEFLATE] = balloon_ack;
names[VIRTIO_BALLOON_VQ_DEFLATE] = "deflate";
callbacks[VIRTIO_BALLOON_VQ_STATS] = NULL;
names[VIRTIO_BALLOON_VQ_STATS] = NULL;
callbacks[VIRTIO_BALLOON_VQ_FREE_PAGE] = NULL;
names[VIRTIO_BALLOON_VQ_FREE_PAGE] = NULL;
names[VIRTIO_BALLOON_VQ_REPORTING] = NULL;
먼저 VIRTIO_BALLOON_VQ_STATS의 정의부터 보자.
enum virtio_balloon_vq {
VIRTIO_BALLOON_VQ_INFLATE,
VIRTIO_BALLOON_VQ_DEFLATE,
VIRTIO_BALLOON_VQ_STATS,
VIRTIO_BALLOON_VQ_FREE_PAGE,
VIRTIO_BALLOON_VQ_REPORTING,
VIRTIO_BALLOON_VQ_MAX
};
정의에서 알 수 있듯이,
VIRTIO_BALLOON_VQ_INFLATE의 값은 0, VIRTIO_BALLOON_VQ_DEFLATE의 값은 1, VIRTIO_BALLOON_VQ_STATS의 값은 2, VIRTIO_BALLOON_VQ_FREE_PAGE의 값은 3, VIRTIO_BALLOON_VQ_REPORTING의 값은 4, VIRTIO_BALLOON_VQ_MAX의 값은 5다.
코드 주석에 따르면, inflateq와 deflateq는 무조건 사용된다.
그러나 관련 feature가 활성화되지 않으면 대응하는 names[i]는 NULL이 될 것이고, 이것은 find_vqs 함수에서 해당하는 virtqueue를 할당하지 않게 한다.
주석을 보면 뒤의 코드도 이해할 수 있다. inldateq와 deflateq는 무조건 사용되기 때문에, 이들이 대응하는 names[VIRTIO_BALLOON_VQ_INFLATE](즉 names[0])와 names[VIRTIO_BALLOON_VQ_DEFLATE](즉 names[1])는 처음부터 NULL이 아니라 각각 "inflate"와 "deflate"로 설정된다.
동시에 각각의 callback도 모두 balloon_ack로 설정된다.
(2) 이어서 나머지 VIRTIO_BALLOON_F_STATS_VQ、VIRTIO_BALLOON_VQ_FREE_PAGE、VIRTIO_BALLOON_VQ_REPORTING 특성이 존재하는지 판단한다.
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_STATS_VQ)) {
names[VIRTIO_BALLOON_VQ_STATS] = "stats";
callbacks[VIRTIO_BALLOON_VQ_STATS] = stats_request;
}
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_FREE_PAGE_HINT)) {
names[VIRTIO_BALLOON_VQ_FREE_PAGE] = "free_page_vq";
callbacks[VIRTIO_BALLOON_VQ_FREE_PAGE] = NULL;
}
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_REPORTING)) {
names[VIRTIO_BALLOON_VQ_REPORTING] = "reporting_vq";
callbacks[VIRTIO_BALLOON_VQ_REPORTING] = balloon_ack;
}
위에서 설명한 대로, 이 세 항목은 모두 무조건 사용되는 feature가 아니다.
그래서 처음 초기화될 때는 기본적으로 사용 안 함으로 간주되어, 대응하는 names[i]와 callbacks[i]를 모두 NULL로 설정한 것이다. 그러고 나서 여기서 해당 특성이 활성화되었는지 판단하는데, 어떤 항목이 활성화되었으면 그 names[i]를 해당 값으로 설정한다. 물론 callbacks[i]도 마찬가지다.
callbacks/names 배열 초기화 상태 정리

/**
* virtio_has_feature - helper to determine if this device has this feature.
* @vdev: the device
* @fbit: the feature bit
*/
static inline bool virtio_has_feature(const struct virtio_device *vdev,
unsigned int fbit)
{
if (fbit < VIRTIO_TRANSPORT_F_START)
virtio_check_driver_offered_feature(vdev, fbit);
return __virtio_test_bit(vdev, fbit);
}
/*
* Virtio feature bits VIRTIO_TRANSPORT_F_START through
* VIRTIO_TRANSPORT_F_END are reserved for the transport
* being used (e.g. virtio_ring, virtio_pci etc.), the
* rest are per-device feature bits.
*/
#define VIRTIO_TRANSPORT_F_START 28
#define VIRTIO_TRANSPORT_F_END 41
여기서 virtio_has_feature 함수의 매개변수 fbit에 대응하는 실제 인자는 각각 VIRTIO_BALLOON_VQ_STATS (2), VIRTIO_BALLOON_VQ_FREE_PAGE (3), VIRTIO_BALLOON_VQ_REPORTING (4)인데, 모두 VIRTIO_TRANSPORT_F_START (28)보다 작다.
그래서 모두 virtio_check_driver_offered_feature 함수를 거치게 된다.
void virtio_check_driver_offered_feature(const struct virtio_device *vdev,
unsigned int fbit)
{
unsigned int i;
struct virtio_driver *drv = drv_to_virtio(vdev->dev.driver);
for (i = 0; i < drv->feature_table_size; i++)
if (drv->feature_table[i] == fbit)
return;
if (drv->feature_table_legacy) {
for (i = 0; i < drv->feature_table_size_legacy; i++)
if (drv->feature_table_legacy[i] == fbit)
return;
}
BUG();
}
EXPORT_SYMBOL_GPL(virtio_check_driver_offered_feature);
virtio_check_driver_offered_feature 함수는 vdev(struct virtio_device 타입)에 해당하는 drv(struct virtio_driver 타입)의 feature_table[]에 대응되는 항목이 있는지 검사한다.
있으면 무탈하게 진행되고, 없으면 BUG()를 보고한다.
virtio_check_driver_offered_feature 함수의 검사를 거친 후, virtio_has_feature 함수는 __virtio_test_bit 함수를 호출하고 반환한다.
/**
* __virtio_test_bit - helper to test feature bits. For use by transports.
* Devices should normally use virtio_has_feature,
* which includes more checks.
* @vdev: the device
* @fbit: the feature bit
*/
static inline bool __virtio_test_bit(const struct virtio_device *vdev,
unsigned int fbit)
{
/* Did you forget to fix assumptions on max features? */
if (__builtin_constant_p(fbit))
BUILD_BUG_ON(fbit >= 64);
else
BUG_ON(fbit >= 64);
return vdev->features & BIT_ULL(fbit);
}
이 함수는 비교적 이해하기 쉽다. vdev->feature에서 해당 비트가 1로 설정되어 있는지 검사하는 것이다. 1로 설정되어 있으면 해당 feature가 이미 활성화된 것이므로 참(True)을 반환하고, 그렇지 않으면 해당 feature가 활성화되지 않은 것이므로 거짓(False)을 반환한다.
다시 init_vqs 함수로 돌아온다. 각 feature에 대한 초기화, 검사 및 설정을 거친 후, 이어서 init_vqs 함수는 virtio_find_vqs 함수를 호출하는데, 이 함수에 대한 분석은 아래에서 진행한다.
virtio_find_vqs 함수 분석
지난 절에서는 virtballoon_probe 함수와 그 안의 두 가지 중요한 함수 init_vqs()와 virtio_device_ready()를 설명했고, init_vqs 함수의 앞 두 단계까지 분석했다. 여기서는 이 함수에 대한 분석을 계속한다.
(3) init_vqs 함수는 각 feature에 대한 초기화, 검사, 설정을 마친 후, 이어서 virtio_find_vqs 함수를 호출한다.
err = virtio_find_vqs(vb->vdev, VIRTIO_BALLOON_VQ_MAX, vqs,
callbacks, names, NULL);
if (err)
return err;
static inline
int virtio_find_vqs(struct virtio_device *vdev, unsigned nvqs,
struct virtqueue *vqs[], vq_callback_t *callbacks[],
const char * const names[],
struct irq_affinity *desc)
{
return vdev->config->find_vqs(vdev, nvqs, vqs, callbacks, names, NULL, desc);
}
사실 구 버전 코드에서는 init_vqs 함수 안에서 virtio_config_ops의 find_vqs 콜백을 직접 호출했었는데, 새 버전 코드에서는 그걸 virtio_find_vqs로 감싸 둔 거다.
virtio_pci_device 핵심 데이터 구조 관계도

virtio_pci_config_ops의 초기화는 두 군데가 있는데,
- /drivers/virtio/virtio_pci_legacy.c
- /drivers/virtio/virtio_pci_modern.c
modern 쪽을 예로 들어보면, struct virtio_config_ops virtio_pci_config_ops 안에서 find_vqs 멤버에 대응되는 함수는 vp_modern_find_vqs()다.
static int vp_modern_find_vqs(struct virtio_device *vdev, unsigned int nvqs,
struct virtqueue *vqs[],
vq_callback_t *callbacks[],
const char * const names[], const bool *ctx,
struct irq_affinity *desc)
{
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
struct virtqueue *vq;
int rc = vp_find_vqs(vdev, nvqs, vqs, callbacks, names, ctx, desc);
if (rc)
return rc;
/* Select and activate all queues. Has to be done last: once we do
* this, there's no way to go back except reset.
*/
list_for_each_entry(vq, &vdev->vqs, list)
vp_modern_set_queue_enable(&vp_dev->mdev, vq->index, true);
return 0;
}
err = virtio_find_vqs(vb->vdev, VIRTIO_BALLOON_VQ_MAX, vqs,
callbacks, names, NULL);
if (err)
return err;
vp_modern_find_vqs 함수는 동일한 매개변수로 vp_find_vqs 함수를 호출했다.
int rc = vp_find_vqs(vdev, nvqs, vqs, callbacks, names, ctx, desc);
if (rc)
return rc;
/* the config->find_vqs() implementation */
int vp_find_vqs(struct virtio_device *vdev, unsigned int nvqs,
struct virtqueue *vqs[], vq_callback_t *callbacks[],
const char * const names[], const bool *ctx,
struct irq_affinity *desc)
{
int err;
/* Try MSI-X with one vector per queue. */
err = vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, true, ctx, desc);
if (!err)
return 0;
/* Fallback: MSI-X with one vector for config, one shared for queues. */
err = vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, false, ctx, desc);
if (!err)
return 0;
/* Is there an interrupt? If not give up. */
if (!(to_vp_device(vdev)->pci_dev->irq))
return err;
/* Finally fall back to regular interrupts. */
return vp_find_vqs_intx(vdev, nvqs, vqs, callbacks, names, ctx);
}
구 버전 Linux 커널의 KVM 코드에서는 vp_find_vqs 함수가 본질적으로는 vp_try_to_find_ops() 함수 하나만 호출했었다. 그런데 새 버전 코드에서는 변화가 좀 커서, 주로 두 개의 함수, 즉 vp_find_vqs_msix()와 vp_find_vqs_intx()를 호출하게 됐다.
vp_find_vqs 인터럽트 방식 선택 흐름

static int vp_find_vqs_msix(struct virtio_device *vdev, unsigned int nvqs,
struct virtqueue *vqs[],
vq_callback_t *callbacks[],
const char * const names[], bool per_vq_vectors,
const bool *ctx,
struct irq_affinity *desc)
{
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
u16 msix_vec;
int i, err, nvectors, allocated_vectors, queue_idx = 0;
vp_dev->vqs = kcalloc(nvqs, sizeof(*vp_dev->vqs), GFP_KERNEL);
if (!vp_dev->vqs)
return -ENOMEM;
if (per_vq_vectors) {
/* Best option: one for change interrupt, one per vq. */
nvectors = 1;
for (i = 0; i < nvqs; ++i)
if (names[i] && callbacks[i])
++nvectors;
} else {
/* Second best: one for change, shared for all vqs. */
nvectors = 2;
}
err = vp_request_msix_vectors(vdev, nvectors, per_vq_vectors,
per_vq_vectors ? desc : NULL);
if (err)
goto error_find;
vp_dev->per_vq_vectors = per_vq_vectors;
allocated_vectors = vp_dev->msix_used_vectors;
for (i = 0; i < nvqs; ++i) {
if (!names[i]) {
vqs[i] = NULL;
continue;
}
if (!callbacks[i])
msix_vec = VIRTIO_MSI_NO_VECTOR;
else if (vp_dev->per_vq_vectors)
msix_vec = allocated_vectors++;
else
msix_vec = VP_MSIX_VQ_VECTOR;
vqs[i] = vp_setup_vq(vdev, queue_idx++, callbacks[i], names[i],
ctx ? ctx[i] : false,
msix_vec);
if (IS_ERR(vqs[i])) {
err = PTR_ERR(vqs[i]);
goto error_find;
}
if (!vp_dev->per_vq_vectors || msix_vec == VIRTIO_MSI_NO_VECTOR)
continue;
/* allocate per-vq irq if available and necessary */
snprintf(vp_dev->msix_names[msix_vec],
sizeof *vp_dev->msix_names,
"%s-%s",
dev_name(&vp_dev->vdev.dev), names[i]);
err = request_irq(pci_irq_vector(vp_dev->pci_dev, msix_vec),
vring_interrupt, 0,
vp_dev->msix_names[msix_vec],
vqs[i]);
if (err)
goto error_find;
}
return 0;
error_find:
vp_del_vqs(vdev);
return err;
}
코드를 봐서는, vp_find_vqs_msix 함수는 구 버전 KVM 코드의 vp_try_to_find_ops 함수와 매우 비슷하다.
(1) vp_find_vqs_msix 함수는 먼저 kcalloc 함수를 통해 struct virtio_pci_vq_info를 가리키는 포인터 nvqs개를 할당하고, virtio_pci_device의 vqs 멤버에 부여한다.
(struct virtio_pci_vq_info **vqs;) 각 virtio_pci_vq_info는 virtqueue의 정보를 기록한다.
vp_dev->vqs = kcalloc(nvqs, sizeof(*vp_dev->vqs), GFP_KERNEL);
if (!vp_dev->vqs)
return -ENOMEM;
nvqs의 실제 인자는 VIRTIO_BALLOON_VQ_MAX로, 앞 절에서 말했듯 이 값은 5다.
주의할 점은, 여기서는 포인터만 할당했지 구체적인 구조체, 즉 각 포인터가 가리키는 공간까지 할당한 건 아니라는 거다.
(2) vp_find_vqs_msix 함수는 이어서 nvectors를 계산한다.
if (per_vq_vectors) {
/* Best option: one for change interrupt, one per vq. */
nvectors = 1;
for (i = 0; i < nvqs; ++i)
if (names[i] && callbacks[i])
++nvectors;
} else {
/* Second best: one for change, shared for all vqs. */
nvectors = 2;
}
nvectors는 총 필요한 MSIx vector를 나타낸다. vp_find_vqs 함수가 vp_find_vqs_msix 함수를 두 번 호출하기 때문에,
/* Try MSI-X with one vector per queue. */
err = vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, true, ctx, desc);
if (!err)
return 0;
/* Fallback: MSI-X with one vector for config, one shared for queues. */
err = vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, false, ctx, desc);
if (!err)
return 0;
그래서 여기서 앞 번 호출로 얻은 nvqs 값은 [2,4] 중 하나가 된다(이전 글에서 말한 조건부 feature가 활성화되었는지 여부에 따라 결정된다). 그리고 뒤 번 호출로 얻은 nvqs 값은 2다.
코드 분석을 계속하기 전에, virtio의 인터럽트에 대해 먼저 알아보자.
virtio에는 두 가지 인터럽트 타입이 있다.
- change 인터럽트
디바이스의 설정 정보가 변경되면(config가 변경됨), 인터럽트가 하나 발생하는데(change 인터럽트라고 부른다), 인터럽트 처리 프로그램은 대응되는 처리 함수를 호출해야 한다(드라이버가 정의해야 함).
- vq 인터럽트
디바이스가 큐에 정보를 쓸 때, 인터럽트가 하나 발생하는데(vq 인터럽트라고 부른다), 인터럽트 처리 함수는 대응하는 큐의 콜백 함수를 호출해야 한다(드라이버가 정의해야 함).
virtio에는 세 가지 인터럽트 처리 방식이 있다.
virtio 인터럽트 처리 방식 비교

1) msix 인터럽트를 사용하지 않고, 일반 인터럽트를 사용
change 인터럽트와 모든 vq 인터럽트가 하나의 인터럽트 irq를 공유한다.
인터럽트 처리 함수는 vp_interrupt이고, vp_interrupt 함수 안에는 change 인터럽트와 vq 인터럽트의 처리가 포함되어 있다.
2) msix 인터럽트를 사용하지만, vector가 두 개만 있는 경우
두 개의 vector 중, 하나는 change 인터럽트에 대응하고, 다른 하나는 모든 큐의 vq 인터럽트에 대응한다.
change 인터럽트 처리 함수는 vp_config_changed이고, vq 인터럽트 처리 함수는 vp_vring_interrupt다.
대응되는 게 vp_find_vqs 함수가 vp_find_vqs_msix 함수를 두 번째로 호출하는 코드다
/* Fallback: MSI-X with one vector for config, one shared for queues. */
err = vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, false, ctx, desc);
if (!err)
return 0;
else {
/* Second best: one for change, shared for all vqs. */
nvectors = 2;
}
3) msix 인터럽트를 사용하고, n+1개의 vector가 있는 경우
n+1개 vector 중, 하나는 change 인터럽트에 대응하고, n개는 각각 n개 큐의 vq 인터럽트에 대응한다. 즉 각 vq가 하나의 vector를 갖는다.
change 인터럽트 처리 함수는 vp_config_changed이고, vq 인터럽트 처리 함수는 vring_interrupt다.
enum virtio_balloon_vq {
VIRTIO_BALLOON_VQ_INFLATE,
VIRTIO_BALLOON_VQ_DEFLATE,
VIRTIO_BALLOON_VQ_STATS,
VIRTIO_BALLOON_VQ_FREE_PAGE,
VIRTIO_BALLOON_VQ_REPORTING,
VIRTIO_BALLOON_VQ_MAX
};
대응되는 게 vp_find_vqs 함수가 vp_find_vqs_msix 함수를 첫 번째로 호출하는 코드다
/* Try MSI-X with one vector per queue. */
err = vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, true, ctx, desc);
if (!err)
return 0;
if (per_vq_vectors) {
/* Best option: one for change interrupt, one per vq. */
nvectors = 1;
for (i = 0; i < nvqs; ++i)
if (names[i] && callbacks[i])
++nvectors;
}
vp_find_vqs 함수의 호출 순서에 따라,
먼저 인터럽트 처리 방식 3(msix 인터럽트 사용, n+1개 vector)을 사용해보고,
그다음 방식 2(msix 인터럽트 사용하지만 vector가 두 개만 있는 경우),
마지막으로 방식 1(msix 인터럽트를 사용하지 않고 일반 인터럽트 사용)을 사용한다.
(3) vp_find_vqs_msix 함수는 이어서 vp_request_msix_vectors 함수를 호출한다.
err = vp_request_msix_vectors(vdev, nvectors, per_vq_vectors,
per_vq_vectors ? desc : NULL);
if (err)
goto error_find;
static int vp_request_msix_vectors(struct virtio_device *vdev, int nvectors,
bool per_vq_vectors, struct irq_affinity *desc)
{
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
const char *name = dev_name(&vp_dev->vdev.dev);
unsigned int flags = PCI_IRQ_MSIX;
unsigned int i, v;
int err = -ENOMEM;
vp_dev->msix_vectors = nvectors;
vp_dev->msix_names = kmalloc_array(nvectors,
sizeof(*vp_dev->msix_names),
GFP_KERNEL);
if (!vp_dev->msix_names)
goto error;
vp_dev->msix_affinity_masks
= kcalloc(nvectors, sizeof(*vp_dev->msix_affinity_masks),
GFP_KERNEL);
if (!vp_dev->msix_affinity_masks)
goto error;
for (i = 0; i < nvectors; ++i)
if (!alloc_cpumask_var(&vp_dev->msix_affinity_masks[i],
GFP_KERNEL))
goto error;
if (desc) {
flags |= PCI_IRQ_AFFINITY;
desc->pre_vectors++; /* virtio config vector */
}
err = pci_alloc_irq_vectors_affinity(vp_dev->pci_dev, nvectors,
nvectors, flags, desc);
if (err < 0)
goto error;
vp_dev->msix_enabled = 1;
/* Set the vector used for configuration */
v = vp_dev->msix_used_vectors;
snprintf(vp_dev->msix_names[v], sizeof *vp_dev->msix_names,
"%s-config", name);
err = request_irq(pci_irq_vector(vp_dev->pci_dev, v),
vp_config_changed, 0, vp_dev->msix_names[v],
vp_dev);
if (err)
goto error;
++vp_dev->msix_used_vectors;
v = vp_dev->config_vector(vp_dev, v);
/* Verify we had enough resources to assign the vector */
if (v == VIRTIO_MSI_NO_VECTOR) {
err = -EBUSY;
goto error;
}
if (!per_vq_vectors) {
/* Shared vector for all VQs */
v = vp_dev->msix_used_vectors;
snprintf(vp_dev->msix_names[v], sizeof *vp_dev->msix_names,
"%s-virtqueues", name);
err = request_irq(pci_irq_vector(vp_dev->pci_dev, v),
vp_vring_interrupt, 0, vp_dev->msix_names[v],
vp_dev);
if (err)
goto error;
++vp_dev->msix_used_vectors;
}
return 0;
error:
return err;
}
핵심 함수 호출 순서 정리

여기까지가 virtio balloon 디바이스의 초기화 과정 전체 분석이다. 핵심은
- virtballoon_probe() 가 디바이스별 진입점이고, virtio_balloon 구조체를 할당/초기화한다.
- init_vqs() 가 virtqueue 배열을 준비하는데, inflateq/deflateq는 무조건 사용되고 나머지 세 개(stats, free_page, reporting)는 feature 활성화 여부에 따라 결정된다.
- virtio_find_vqs() → vp_modern_find_vqs() → vp_find_vqs() → vp_find_vqs_msix() 의 호출 체인을 거치는데, 이 중에서 인터럽트 방식 3 → 2 → 1 순으로 시도한다.
- vp_request_msix_vectors() 가 실제로 MSI-X vector를 할당하고 IRQ를 등록한다. 이 함수에 대한 자세한 분석은 다음 회로 미뤄진다.
- 모든 초기화가 끝나면 virtio_device_ready() 로 DRIVER_OK 비트를 세팅해서 디바이스가 사용 가능 상태가 된다.
vp_find_vqs_msix 와 vp_setup_vq
지금까지 init_vqs 함수에서 호출한 virtio_find_vqs 함수를 따라 들어가서, virtio_find_vqs 함수 안에서 호출하는 vdev->config->find_vqs(vdev, nvqs, vqs, callbacks, names, NULL, desc) (즉 virtio_config_ops의 find_vqs 콜백)를 통해, 이 콜백이 가리키는 함수 vp_modern_find_vqs()까지 따라갔고, 다시 그것이 소속된 vp_find_vqs 함수까지 따라간다. 호출 흐름은 다음과 같다.

vp_find_vqs 함수는 인터럽트 방식을 세 가지 우선순위로 시도한다.

지금까지 vp_find_vqs_msix 함수까지 이야기했고, 그중에서 앞 두 단계를 다뤘다.
세 번째 단계에서 vp_request_msix_vectors 함수를 호출하는 부분에 이르러서, 다시 한 층 더 따라 들어갔다.
이제부터 vp_request_msix_vectors 함수에 대해 자세히 분석한다.
static int vp_request_msix_vectors(struct virtio_device *vdev, int nvectors,
bool per_vq_vectors)
{
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
const char *name = dev_name(&vp_dev->vdev.dev);
unsigned i, v;
int err = -ENOMEM;
vp_dev->msix_vectors = nvectors;
vp_dev->msix_entries = kmalloc(nvectors * sizeof *vp_dev->msix_entries,
GFP_KERNEL);
if (!vp_dev->msix_entries)
goto error;
vp_dev->msix_names = kmalloc(nvectors * sizeof *vp_dev->msix_names,
GFP_KERNEL);
if (!vp_dev->msix_names)
goto error;
vp_dev->msix_affinity_masks
= kzalloc(nvectors * sizeof *vp_dev->msix_affinity_masks,
GFP_KERNEL);
if (!vp_dev->msix_affinity_masks)
goto error;
for (i = 0; i < nvectors; ++i)
if (!alloc_cpumask_var(&vp_dev->msix_affinity_masks[i],
GFP_KERNEL))
goto error;
for (i = 0; i < nvectors; ++i)
vp_dev->msix_entries[i].entry = i;
err = pci_enable_msix_exact(vp_dev->pci_dev,
vp_dev->msix_entries, nvectors);
if (err)
goto error;
vp_dev->msix_enabled = 1;
/* Set the vector used for configuration */
v = vp_dev->msix_used_vectors;
snprintf(vp_dev->msix_names[v], sizeof *vp_dev->msix_names,
"%s-config", name);
err = request_irq(vp_dev->msix_entries[v].vector,
vp_config_changed, 0, vp_dev->msix_names[v],
vp_dev);
if (err)
goto error;
++vp_dev->msix_used_vectors;
v = vp_dev->config_vector(vp_dev, v);
/* Verify we had enough resources to assign the vector */
if (v == VIRTIO_MSI_NO_VECTOR) {
err = -EBUSY;
goto error;
}
if (!per_vq_vectors) {
/* Shared vector for all VQs */
v = vp_dev->msix_used_vectors;
snprintf(vp_dev->msix_names[v], sizeof *vp_dev->msix_names,
"%s-virtqueues", name);
err = request_irq(vp_dev->msix_entries[v].vector,
vp_vring_interrupt, 0, vp_dev->msix_names[v],
vp_dev);
if (err)
goto error;
++vp_dev->msix_used_vectors;
}
return 0;
error:
vp_free_vectors(vdev);
return err;
}
옛 버전과 새 버전 vp_request_msix_vectors 함수의 가장 큰 차이는, 새 버전에서는 vp_dev->msix_entries 관련 코드가 사라졌다는 점이다. 이는 새 버전의 virtio_pci_device 구조체 안에 더 이상 msix_entries라는 멤버가 없기 때문에 그런 것이다.
옛 버전의 vp_request_msix_vectors 함수는 다음 작업을 완료했다 (virtio 디바이스 인터럽트 분석 참고).
- nvectors 개의 msix 인터럽트에 vector를 할당하고, 그중 1개의 vector를 vp_config_changed라는 change 인터럽트 처리 함수를 지정하는 데 사용한다.
- 만약 per_vq_vectors가 false라면, nvectors는 곧 2가 되고, 남는 또 하나의 vector를 n개 vq에서 공유로 사용하는 vq 인터럽트 처리 함수 vp_vring_interrupt를 지정하는 데 쓴다.
- 만약 per_vq_vectors가 true라면, 아래쪽 코드에서 매 개 vq별로 하나씩 vector를 지정하고, vq 인터럽트 처리 함수를 vring_interrupt로 지정한다.

새 버전과 옛 버전 코드의 차이가 기능적으로는 큰 연관이 없기 때문에, 신/구 버전 vp_request_msix_vectors 함수의 기능은 위에서 서술한 것과 기본적으로 일치한다.
(4) vp_find_vqs_msix 함수
vp_dev->per_vq_vectors = per_vq_vectors;
allocated_vectors = vp_dev->msix_used_vectors;
첫 번째 코드의 뜻은 vp_find_vqs_msix 함수의 매개변수 bool per_vq_vectors를 vp_dev(virtio PCI 디바이스)의 per_vq_vectors 멤버에 할당하는 것이다.
두 번째 코드는 vp_dev->msix_used_vectors 즉 사용된 인터럽트 벡터 개수를 allocated_vectors에 할당한다.
vp_dev->msix_used_vectors는 위쪽의 vp_request_msix_vectors 함수에서 생성된다.
/* Set the vector used for configuration */
v = vp_dev->msix_used_vectors;
snprintf(vp_dev->msix_names[v], sizeof *vp_dev->msix_names,
"%s-config", name);
err = request_irq(pci_irq_vector(vp_dev->pci_dev, v),
vp_config_changed, 0, vp_dev->msix_names[v],
vp_dev);
if (err)
goto error;
++vp_dev->msix_used_vectors;
v = vp_dev->config_vector(vp_dev, v);
/* Verify we had enough resources to assign the vector */
if (v == VIRTIO_MSI_NO_VECTOR) {
err = -EBUSY;
goto error;
}
if (!per_vq_vectors) {
/* Shared vector for all VQs */
v = vp_dev->msix_used_vectors;
snprintf(vp_dev->msix_names[v], sizeof *vp_dev->msix_names,
"%s-virtqueues", name);
err = request_irq(pci_irq_vector(vp_dev->pci_dev, v),
vp_vring_interrupt, 0, vp_dev->msix_names[v],
vp_dev);
if (err)
goto error;
++vp_dev->msix_used_vectors;
}
(5) 다음으로, vp_find_vqs_msix 함수가 루프에 진입하여, 각 사용 가능한 vq에 대해 설정하고 인터럽트를 요청한다.
enum virtio_balloon_vq {
VIRTIO_BALLOON_VQ_INFLATE,
VIRTIO_BALLOON_VQ_DEFLATE,
VIRTIO_BALLOON_VQ_STATS,
VIRTIO_BALLOON_VQ_FREE_PAGE,
VIRTIO_BALLOON_VQ_REPORTING,
VIRTIO_BALLOON_VQ_MAX
};
for (i = 0; i < nvqs; ++i) {
if (!names[i]) {
vqs[i] = NULL;
continue;
}
if (!callbacks[i])
msix_vec = VIRTIO_MSI_NO_VECTOR;
else if (vp_dev->per_vq_vectors)
msix_vec = allocated_vectors++;
else
msix_vec = VP_MSIX_VQ_VECTOR;
vqs[i] = vp_setup_vq(vdev, queue_idx++, callbacks[i], names[i],
ctx ? ctx[i] : false,
msix_vec);
if (IS_ERR(vqs[i])) {
err = PTR_ERR(vqs[i]);
goto error_find;
}
if (!vp_dev->per_vq_vectors || msix_vec == VIRTIO_MSI_NO_VECTOR)
continue;
/* allocate per-vq irq if available and necessary */
snprintf(vp_dev->msix_names[msix_vec],
sizeof *vp_dev->msix_names,
"%s-%s",
dev_name(&vp_dev->vdev.dev), names[i]);
err = request_irq(pci_irq_vector(vp_dev->pci_dev, msix_vec),
vring_interrupt, 0,
vp_dev->msix_names[msix_vec],
vqs[i]);
if (err)
goto error_find;
}
조건 없는 feature가 2개 있는데, 다음과 같다.
struct virtqueue *vqs[VIRTIO_BALLOON_VQ_MAX];
vq_callback_t *callbacks[VIRTIO_BALLOON_VQ_MAX];
const char *names[VIRTIO_BALLOON_VQ_MAX];
int err;
/*
* Inflateq and deflateq are used unconditionally. The names[]
* will be NULL if the related feature is not enabled, which will
* cause no allocation for the corresponding virtqueue in find_vqs.
*/
callbacks[VIRTIO_BALLOON_VQ_INFLATE] = balloon_ack;
names[VIRTIO_BALLOON_VQ_INFLATE] = "inflate";
callbacks[VIRTIO_BALLOON_VQ_DEFLATE] = balloon_ack;
names[VIRTIO_BALLOON_VQ_DEFLATE] = "deflate";
callbacks[VIRTIO_BALLOON_VQ_STATS] = NULL;
names[VIRTIO_BALLOON_VQ_STATS] = NULL;
callbacks[VIRTIO_BALLOON_VQ_FREE_PAGE] = NULL;
names[VIRTIO_BALLOON_VQ_FREE_PAGE] = NULL;
callbacks[VIRTIO_BALLOON_VQ_REPORTING] = NULL;
조건이 있는 feature는 3개 있는데, 다음과 같다.
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_STATS_VQ)) {
names[VIRTIO_BALLOON_VQ_STATS] = "stats";
callbacks[VIRTIO_BALLOON_VQ_STATS] = stats_request;
}
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_FREE_PAGE_HINT)) {
names[VIRTIO_BALLOON_VQ_FREE_PAGE] = "free_page_vq";
callbacks[VIRTIO_BALLOON_VQ_FREE_PAGE] = NULL;
}
if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_REPORTING)) {
names[VIRTIO_BALLOON_VQ_REPORTING] = "reporting_vq";
callbacks[VIRTIO_BALLOON_VQ_REPORTING] = balloon_ack;
}
만약 어떤 feature가 활성화되지 않았다면, 그것에 대응하는 names[i]가 NULL이 되고, 그러면 그에 대응하는 vqs[i]도 NULL로 설정하고 그 특성을 건너뛴다.
if (!names[i]) {
vqs[i] = NULL;
continue;
}
여기서 더 아래로 갈 수 있는 것은 활성화된 feature뿐이다.
여기는 다시 두 가지 경우로 나뉜다.
- 하나는 그에 대응하는 callbacks[i]가 NULL이 아닌 경우인데, 이는 대다수의 feature에 해당된다.
- 또 하나는 비록 names[i]는 비어있지 않지만 callbacks[i]가 NULL인 경우인데, 현재 이런 상황은VIRTIO_BALLOON_VQ_FREE_PAGE에서만 나타난다.
후자의 경우에는 msix_vec을 VIRTIO_MSI_NO_VECTOR로 설정한다.
전자의 경우에는, 또 위에서 다룬 virtio의 인터럽트 처리 방식에 따라 구별해서 처리해야 한다.
"msix 인터럽트를 사용하고, n+1개의 vector가 있는" 경우라면, 매 루프마다 한 번씩 allocated_vectors를 1씩 증가시키며 동시에 msix_vec에 할당한다. 그렇지 않으면 msix_vec을 VP_MSIX_VQ_VECTOR로 설정한다.
if (!callbacks[i])
msix_vec = VIRTIO_MSI_NO_VECTOR;
else if (vp_dev->per_vq_vectors)
msix_vec = allocated_vectors++;
else
msix_vec = VP_MSIX_VQ_VECTOR;
루프 내의 분기 흐름을 정리하면 다음과 같다.

다음으로, 루프 안에서 첫 번째 핵심 함수에 도달하게 된다. vp_setup_vq다
vp_setup_vq 함수 분석
지금까지 vp_find_vqs_msix 함수의 5단계까지 이야기했는데, 루프에 진입해서 각 사용 가능한 vq에 대해 설정하고 인터럽트를 요청하는 부분이다. 이번 파트에서는 첫 번째 핵심 함수 vp_setup_vq를 분석한다.
static struct virtqueue *vp_setup_vq(struct virtio_device *vdev, unsigned int index,
void (*callback)(struct virtqueue *vq),
const char *name,
bool ctx,
u16 msix_vec)
{
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
struct virtio_pci_vq_info *info = kmalloc(sizeof *info, GFP_KERNEL);
struct virtqueue *vq;
unsigned long flags;
/* fill out our structure that represents an active queue */
if (!info)
return ERR_PTR(-ENOMEM);
vq = vp_dev->setup_vq(vp_dev, info, index, callback, name, ctx,
msix_vec);
if (IS_ERR(vq))
goto out_info;
info->vq = vq;
if (callback) {
spin_lock_irqsave(&vp_dev->lock, flags);
list_add(&info->node, &vp_dev->virtqueues);
spin_unlock_irqrestore(&vp_dev->lock, flags);
} else {
INIT_LIST_HEAD(&info->node);
}
vp_dev->vqs[index] = info;
return vq;
out_info:
kfree(info);
return vq;
}
vp_setup_vq 함수는 virtqueue를 초기화한다. 이 함수 안에서 구체적인 virtio_pci_vq_info 구조체 객체 하나를 할당해서, 하나의 virtqueue 정보를 표현한다.
struct virtio_pci_vq_info *info = kmalloc(sizeof *info, GFP_KERNEL);
그리고 그 객체를 매개변수로 해서, virtio_pci_device의 setup_vq 콜백 함수를 호출한다.
vq = vp_dev->setup_vq(vp_dev, info, index, callback, name, ctx,
msix_vec);
if (IS_ERR(vq))
goto out_info;
setup_vq 콜백 함수 역시 virtio_pci_modern_probe 함수 안에서 설정된 것이다
static struct virtqueue *setup_vq(struct virtio_pci_device *vp_dev,
struct virtio_pci_vq_info *info,
unsigned int index,
void (*callback)(struct virtqueue *vq),
const char *name,
bool ctx,
u16 msix_vec)
{
struct virtio_pci_modern_device *mdev = &vp_dev->mdev;
bool (*notify)(struct virtqueue *vq);
struct virtqueue *vq;
u16 num;
int err;
if (__virtio_test_bit(&vp_dev->vdev, VIRTIO_F_NOTIFICATION_DATA))
notify = vp_notify_with_data;
else
notify = vp_notify;
if (index >= vp_modern_get_num_queues(mdev))
return ERR_PTR(-EINVAL);
/* Check if queue is either not available or already active. */
num = vp_modern_get_queue_size(mdev, index);
if (!num || vp_modern_get_queue_enable(mdev, index))
return ERR_PTR(-ENOENT);
info->msix_vector = msix_vec;
/* create the vring */
vq = vring_create_virtqueue(index, num,
SMP_CACHE_BYTES, &vp_dev->vdev,
true, true, ctx,
notify, callback, name);
if (!vq)
return ERR_PTR(-ENOMEM);
vq->num_max = num;
err = vp_active_vq(vq, msix_vec);
if (err)
goto err;
vq->priv = (void __force *)vp_modern_map_vq_notify(mdev, index, NULL);
if (!vq->priv) {
err = -ENOMEM;
goto err;
}
return vq;
err:
vring_del_virtqueue(vq);
return ERR_PTR(err);
}
(1) setup_vq 함수는 먼저 virtio PCI 디바이스가 VIRTIO_F_NOTIFICATION_DATA 특성을 갖고 있는지 검사한다.
if (__virtio_test_bit(&vp_dev->vdev, VIRTIO_F_NOTIFICATION_DATA))
notify = vp_notify_with_data;
else
notify = vp_notify;
/*
* This feature indicates that the driver passes extra data (besides
* identifying the virtqueue) in its device notifications.
*/
#define VIRTIO_F_NOTIFICATION_DATA 38
만약 디바이스가 VIRTIO_F_NOTIFICATION_DATA를 지원한다면, notify할 때 (추가) 데이터를 같이 전달한다는 뜻이라서, notify 함수 포인터를 vp_notify_with_data로 설정한다. 즉 vp_notify_with_data 함수를 가리키게 한다. 그렇지 않으면 vp_notify 함수를 가리키게 한다.

static bool vp_notify_with_data(struct virtqueue *vq)
{
u32 data = vring_notification_data(vq);
iowrite32(data, (void __iomem *)vq->priv);
return true;
}
/* the notify function used when creating a virt queue */
bool vp_notify(struct virtqueue *vq)
{
/* we write the queue's selector into the notification register to
* signal the other end */
iowrite16(vq->index, (void __iomem *)vq->priv);
return true;
}
(2) 다음으로, vp_modern_get_num_queues 함수를 호출해서 virtqueues의 길이(개수)를 얻어온다.
if (index >= vp_modern_get_num_queues(mdev))
return ERR_PTR(-EINVAL);
/*
* vp_modern_get_num_queues - get the number of virtqueues
* @mdev: the modern virtio-pci device
*
* Returns the number of virtqueues
*/
u16 vp_modern_get_num_queues(struct virtio_pci_modern_device *mdev)
{
return vp_ioread16(&mdev->common->num_queues);
}
EXPORT_SYMBOL_GPL(vp_modern_get_num_queues);
(3) 다음으로, vp_modern_get_queue_size 함수를 호출해서 한 virtqueue의 크기를 얻어온다.
/* Check if queue is either not available or already active. */
num = vp_modern_get_queue_size(mdev, index);
if (!num || vp_modern_get_queue_enable(mdev, index))
return ERR_PTR(-ENOENT);
/*
* vp_modern_get_queue_size - get size for a virtqueue
* @mdev: the modern virtio-pci device
* @index: the queue index
*
* Returns the size of the virtqueue
*/
u16 vp_modern_get_queue_size(struct virtio_pci_modern_device *mdev,
u16 index)
{
vp_iowrite16(index, &mdev->common->queue_select);
return vp_ioread16(&mdev->common->queue_size);
}
EXPORT_SYMBOL_GPL(vp_modern_get_queue_size);
vp_modern_get_queue_size 함수는 먼저 어떤 virtqueue를 선택하고, 그다음 그것의 크기를 얻어온다.
/*
* vp_modern_get_queue_enable - enable a virtqueue
* @mdev: the modern virtio-pci device
* @index: the queue index
*
* Returns whether a virtqueue is enabled or not
*/
bool vp_modern_get_queue_enable(struct virtio_pci_modern_device *mdev,
u16 index)
{
vp_iowrite16(index, &mdev->common->queue_select);
return vp_ioread16(&mdev->common->queue_enable);
}
EXPORT_SYMBOL_GPL(vp_modern_get_queue_enable);
vp_modern_get_queue_enable 함수는 선택된 virtqueue가 활성화되어 있는지 얻어온다.
여기서 특별히 짚고 넘어가야 하는데, 옛 버전 코드의 setup_vq 함수에서는 시작하자마자 먼저 virtio_pci_device의 common 멤버를 얻어왔다.
struct virtio_pci_common_cfg __iomem *cfg = vp_dev->common;
새 버전에서는 비록 직접 구체적인 common->x를 사용하고, 별도로 중간 변수를 써서 virtio_pci_device의 common 멤버를 따로 저장하지는 않지만, 의미는 같다.
이건 virtio PCI proxy 디바이스에서 설정에 사용되는 MMIO 영역인데, 다음 그림 가운데 부분처럼 보면 된다.

이 주소들을 직접 읽고 쓰면 QEMU의 virtio_pci_common_read/write 콜백 함수로 진입한다.
여기서 common의 각 오프셋과 그에 대응하는 레지스터 이름을 나열해서 대조하기 편하도록 했다
/* Macro versions of offsets for the Old Timers! */
#define VIRTIO_PCI_CAP_VNDR 0
#define VIRTIO_PCI_CAP_NEXT 1
#define VIRTIO_PCI_CAP_LEN 2
#define VIRTIO_PCI_CAP_CFG_TYPE 3
#define VIRTIO_PCI_CAP_BAR 4
#define VIRTIO_PCI_CAP_OFFSET 8
#define VIRTIO_PCI_CAP_LENGTH 12
#define VIRTIO_PCI_NOTIFY_CAP_MULT 16
#define VIRTIO_PCI_COMMON_DFSELECT 0
#define VIRTIO_PCI_COMMON_DF 4
#define VIRTIO_PCI_COMMON_GFSELECT 8
#define VIRTIO_PCI_COMMON_GF 12
#define VIRTIO_PCI_COMMON_MSIX 16
#define VIRTIO_PCI_COMMON_NUMQ 18
#define VIRTIO_PCI_COMMON_STATUS 20
#define VIRTIO_PCI_COMMON_CFGGENERATION 21
#define VIRTIO_PCI_COMMON_Q_SELECT 22
#define VIRTIO_PCI_COMMON_Q_SIZE 24
#define VIRTIO_PCI_COMMON_Q_MSIX 26
#define VIRTIO_PCI_COMMON_Q_ENABLE 28
#define VIRTIO_PCI_COMMON_Q_NOFF 30
#define VIRTIO_PCI_COMMON_Q_DESCLO 32
#define VIRTIO_PCI_COMMON_Q_DESCHI 36
#define VIRTIO_PCI_COMMON_Q_AVAILLO 40
#define VIRTIO_PCI_COMMON_Q_AVAILHI 44
#define VIRTIO_PCI_COMMON_Q_USEDLO 48
#define VIRTIO_PCI_COMMON_Q_USEDHI 52
#define VIRTIO_PCI_COMMON_Q_NDATA 56
#define VIRTIO_PCI_COMMON_Q_RESET 58
현재 위치 - virtqueue 생성 흐름
현재 virtqueue 생성 흐름의 다음 단계(빨간 사각형 안)에 위치한다

전체 vp_find_vqs부터 vp_setup_vq까지의 호출 흐름을 정리하면 다음과 같다.

지금까지 setup_vq 함수의 앞 3단계를 분석했고, 이제 나머지 단계를 이어서 분석한다.
다시 복습을 하면
(1) VIRTIO_F_NOTIFICATION_DATA 특성 검사
setup_vq 함수는 먼저 virtio PCI 디바이스가 VIRTIO_F_NOTIFICATION_DATA 특성을 가지는지 검사한다.
if (__virtio_test_bit(&vp_dev->vdev, VIRTIO_F_NOTIFICATION_DATA))
notify = vp_notify_with_data;
else
notify = vp_notify;
(2) virtqueue의 개수 얻어오기
그 다음으로, vp_modern_get_num_queues 함수를 호출해서 virtqueue의 길이(개수)를 얻어온다.
if (index >= vp_modern_get_num_queues(mdev))
return ERR_PTR(-EINVAL);
(3) virtqueue의 크기 얻어오기
그 다음으로, vp_modern_get_queue_size 함수를 호출해서 한 virtqueue의 크기를 얻어온다.
/* Check if queue is either not available or already active. */
num = vp_modern_get_queue_size(mdev, index);
if (!num || vp_modern_get_queue_enable(mdev, index))
return ERR_PTR(-ENOENT);
(4) vring_create_virtqueue 호출해서 virtqueue 생성
그 다음으로, vring_create_virtqueue 함수를 호출해서 실제로 virtqueue를 생성한다.
struct virtqueue *vring_create_virtqueue(
unsigned int index,
unsigned int num,
unsigned int vring_align,
struct virtio_device *vdev,
bool weak_barriers,
bool may_reduce_num,
bool context,
bool (*notify)(struct virtqueue *),
void (*callback)(struct virtqueue *),
const char *name)
{
if (virtio_has_feature(vdev, VIRTIO_F_RING_PACKED))
return vring_create_virtqueue_packed(index, num, vring_align,
vdev, weak_barriers, may_reduce_num,
context, notify, callback, name, vdev->dev.parent);
return vring_create_virtqueue_split(index, num, vring_align,
vdev, weak_barriers, may_reduce_num,
context, notify, callback, name, vdev->dev.parent);
}
EXPORT_SYMBOL_GPL(vring_create_virtqueue);
vring_create_virtqueue 함수는 virtio device가 VIRTIO_F_RING_PACKED라는 feature를 가지고 있는지에 따라 분기 처리를 한다.

/* This feature indicates support for the packed virtqueue layout. */
#define VIRTIO_F_RING_PACKED 34
vring_create_virtqueue 함수는 백엔드 디바이스가 VIRTIO_F_RING_PACKED라는 feature를 지원하는지에 따라 분기 처리를 한다.
이 특성을 지원하면 vring_create_virtqueue_packed 함수를 호출하고, 그렇지 않으면 vring_create_virtqueue_split 함수를 호출한다. 하나씩 살펴보자.
vring_create_virtqueue_packed 함수
static struct virtqueue *vring_create_virtqueue_packed(
unsigned int index,
unsigned int num,
unsigned int vring_align,
struct virtio_device *vdev,
bool weak_barriers,
bool may_reduce_num,
bool context,
bool (*notify)(struct virtqueue *),
void (*callback)(struct virtqueue *),
const char *name,
struct device *dma_dev)
{
struct vring_virtqueue_packed vring_packed = {};
struct vring_virtqueue *vq;
int err;
if (vring_alloc_queue_packed(&vring_packed, vdev, num, dma_dev))
goto err_ring;
vq = kmalloc(sizeof(*vq), GFP_KERNEL);
if (!vq)
goto err_vq;
vq->vq.callback = callback;
vq->vq.vdev = vdev;
vq->vq.name = name;
vq->vq.index = index;
vq->vq.reset = false;
vq->we_own_ring = true;
vq->notify = notify;
vq->weak_barriers = weak_barriers;
#ifdef CONFIG_VIRTIO_HARDEN_NOTIFICATION
vq->broken = true;
#else
vq->broken = false;
#endif
vq->packed_ring = true;
vq->dma_dev = dma_dev;
vq->use_dma_api = vring_use_dma_api(vdev);
vq->premapped = false;
vq->do_unmap = vq->use_dma_api;
vq->indirect = virtio_has_feature(vdev, VIRTIO_RING_F_INDIRECT_DESC) &&
!context;
vq->event = virtio_has_feature(vdev, VIRTIO_RING_F_EVENT_IDX);
if (virtio_has_feature(vdev, VIRTIO_F_ORDER_PLATFORM))
vq->weak_barriers = false;
err = vring_alloc_state_extra_packed(&vring_packed);
if (err)
goto err_state_extra;
virtqueue_vring_init_packed(&vring_packed, !!callback);
virtqueue_init(vq, num);
virtqueue_vring_attach_packed(vq, &vring_packed);
spin_lock(&vdev->vqs_list_lock);
list_add_tail(&vq->vq.list, &vdev->vqs);
spin_unlock(&vdev->vqs_list_lock);
return &vq->vq;
err_state_extra:
kfree(vq);
err_vq:
vring_free_packed(&vring_packed, vdev, dma_dev);
err_ring:
return NULL;
}
vring_create_virtqueue_packed 함수는 Linux 커널에서 packed ring 기반의 virtqueue를 하나 생성하는 데 사용되고, 해당 virtqueue를 가리키는 포인터를 반환한다.
일부 파라미터 설명은 다음과 같다
- index: virtqueue의 인덱스 번호를 나타낸다.
- num: virtqueue의 개수를 나타낸다.
- vdev: virtio_device 구조체를 가리키는 포인터로, virtqueue와 연관된 디바이스를 나타낸다.
- notify: 함수 포인터(콜백) 하나로, 데이터 전송 완료 후 호스트에 알릴 때 쓴다.
- callback: 함수 포인터(콜백) 하나로, 호스트의 알림을 받았을 때 그에 맞는 동작을 실행하는 데 쓴다.
- name: virtqueue의 이름을 지정한다.
반환값
함수가 성공적으로 실행되면, vring_virtqueue 구조체를 가리키는 포인터를 반환하는데, 이 구조체에는 packed ring을 관리하는 데 필요한 정보들(descriptor 테이블, 사용 가능 ring, 사용된 ring 등)이 들어 있다.
vring_create_virtqueue_split 함수
static struct virtqueue *vring_create_virtqueue_split(
unsigned int index,
unsigned int num,
unsigned int vring_align,
struct virtio_device *vdev,
bool weak_barriers,
bool may_reduce_num,
bool context,
bool (*notify)(struct virtqueue *),
void (*callback)(struct virtqueue *),
const char *name,
struct device *dma_dev)
{
struct vring_virtqueue_split vring_split = {};
struct virtqueue *vq;
int err;
err = vring_alloc_queue_split(&vring_split, vdev, num, vring_align,
may_reduce_num, dma_dev);
if (err)
return NULL;
vq = __vring_new_virtqueue(index, &vring_split, vdev, weak_barriers,
context, notify, callback, name, dma_dev);
if (!vq) {
vring_free_split(&vring_split, vdev, dma_dev);
return NULL;
}
to_vvq(vq)->we_own_ring = true;
return vq;
}
vring_create_virtqueue_split 함수는 Linux 커널에서 split ring 기반의 virtqueue를 하나 생성하는 데 사용되고, 해당 virtqueue를 가리키는 포인터를 반환한다.
이 함수의 주요 파라미터는 다음과 같다
- index: virtqueue의 인덱스 번호를 나타낸다.
- num: virtqueue의 개수를 나타낸다.
- vdev: virtio_device 구조체를 가리키는 포인터로, virtqueue와 연관된 디바이스를 나타낸다.
- notify: 함수 포인터(콜백) 하나로, 데이터 전송 완료 후 호스트에 알릴 때 쓴다.
- callback: 함수 포인터(콜백) 하나로, 호스트의 알림을 받았을 때 그에 맞는 동작을 실행하는 데 쓴다.
- name: virtqueue의 이름을 지정한다.
반환값:
함수가 성공적으로 실행되면, virtqueue 구조체를 가리키는 포인터를 반환한다.
※ split ring과 packed ring
split ring은 descriptor ring, available ring, used ring 이렇게 3개의 데이터 구조를 사용해서 표현하는데, 이게 바로 split ring이라고 불리는 이유다.
프런트엔드는 생산자로서 새로 만든 descriptor를 백엔드가 쓸 수 있도록 available ring에 업데이트하고, 그 다음 백엔드가 available ring에서 자신이 사용 가능한 descriptor를 읽어와서 사용하고, 사용을 끝낸 후에는 다 쓴 descriptor를 used ring에 써서 프런트엔드가 처리하도록 해서, 이 메모리들을 순환적으로 사용하는 방식이다.
여기서 말하는 사용이라는 건, 다른 곳에서 데이터를 이 descriptor가 기술하는 메모리 영역으로 복사해 오는 것일 수도 있고, 이 descriptor가 기술하는 메모리 영역에서 다른 곳으로 데이터를 복사해 가는 것일 수도 있다.

위 그림에서 보듯이, split ring은 프런트엔드와 백엔드 사이에 데이터를 주고받기 위해 descriptor table, avail ring, used ring 세 개의 데이터 구조를 동시에 다뤄야 한다.
packed ring은 Virtio Spec 1.1에서 제시한 새로운 ring 구조로, 프런트엔드와 백엔드 사이의 통신 로직은 split ring과 기본적으로 비슷하지만,
ring 구조에 약간의 변화가 생겼는데, split ring에서 원래 분리되어 있던 ring 구조를 합쳐서 전부 하나의 ring으로 표현한다.
비록 동작 면에서는 확실히 좀 더 복잡해졌지만, 프런트엔드와 백엔드에서 패킷을 주고받을 때 packed ring 모드에서는 ring 구조 하나만 접근하면 되기 때문에, split ring이 매번 ring 3개를 조작해야 하는 것에 비해, packed ring은 접근해야 할 메모리 공간을 줄여서, cache 입장에서 더 친화적이다.
다음 그림은 packed ring의 ring 구조다(이 ring은 프런트엔드와 백엔드 모두에서 볼 수 있다)

descriptor 안의 flag 비트 구성은 다음과 같다:
| 비트 위치 | 의미 |
| 0 | next |
| 1 | write |
| 2 | reserved |
| 3 | indirect |
| 4~6 | reserved |
| 7 | avail |
| 8~14 | reserved |
| 15 | used |
- descriptor 안의 플래그 비트를 통해 이 descriptor의 소유권이 프런트엔드에 있는지 백엔드에 있는지를 판단한다.
- 백엔드는 두 개의 포인터를 사용해서 생산과 소비의 진척을 추적하는데, 각각 last_avail_idx와 last_used_idx다.
- 프런트엔드는 생산자로서 이 descriptor들을 초기화하고, 백엔드는 소비자로서 이 descriptor들을 계속 가져다 쓰고, 사용을 끝낸 후에는 다 쓴 descriptor를 다시 이 ring에 써넣고, 프런트엔드는 이 descriptor가 다시 써진 걸 관찰하고 이 descriptor를 갱신해서 다음 번에 계속 쓸 수 있게 한다.
- 한 가지 언급할 만한 점은, 프런트엔드는 ring의 순서대로 이 descriptor들을 조작하고, 백엔드 역시 완성된 순서대로 이 descriptor들을 조작한다는 것이고, 게다가 in-order 모드에서 descriptor의 가용성은 반드시 연속적인데, 만약 X번 descriptor가 사용 불가능하다면, 기본적으로 X+1번 descriptor도 똑같이 사용 불가능하다. 이렇게 프런트엔드와 백엔드의 협력 메커니즘이 packed ring의 기본 정보 전달 방식을 구성한다.

현재 virtqueue 생성 흐름의 다음 단계(빨간 네모 상자 안)에 위치한다

vring_create_virtqueue_split 함수에서 사용되는 각 구조체 상세 분석
지금까지 setup_vq의 4단계(vring_create_virtqueue 호출)를 다뤘는데, 거기서 두 함수가 등장했다.
- vring_create_virtqueue_packed()
- vring_create_virtqueue_split()
지난 번에는 두 함수의 역할을 거시적으로만 확인했고, 이번에는 두 함수의 구체적인 내용에 대해 각각 상세히 분석한다.
구체적인 함수 기능을 분석하기 전에, 먼저 함수에서 사용된 각 구조체에 대해 상세히 확인할 필요가 있다. 그래야 함수 코드와 기능을 더 잘 이해할 수 있다.
(1) virtio_device 구조
/**
* struct virtio_device - representation of a device using virtio
* @index: unique position on the virtio bus
* @failed: saved value for VIRTIO_CONFIG_S_FAILED bit (for restore)
* @config_enabled: configuration change reporting enabled
* @config_change_pending: configuration change reported while disabled
* @config_lock: protects configuration change reporting
* @vqs_list_lock: protects @vqs.
* @dev: underlying device.
* @id: the device type identification (used to match it with a driver).
* @config: the configuration ops for this device.
* @vringh_config: configuration ops for host vrings.
* @vqs: the list of virtqueues for this device.
* @features: the features supported by both driver and device.
* @priv: private pointer for the driver's use.
*/
struct virtio_device {
int index;
bool failed;
bool config_enabled;
bool config_change_pending;
spinlock_t config_lock;
spinlock_t vqs_list_lock;
struct device dev;
struct virtio_device_id id;
const struct virtio_config_ops *config;
const struct vringh_config_ops *vringh_config;
struct list_head vqs;
u64 features;
void *priv;
};
struct virtio_device는 Linux 커널에서 virtio 디바이스를 표현하는 데 사용되는 데이터 구조로, virtio 디바이스와 관련된 각종 정보와 동작 함수들이 들어 있다.

struct virtio_device의 각 멤버에 대한 상세 설명은 다음과 같다
- int index: virtio 버스(bus) 상의 유일한 위치다.
- bool failed: VIRTIO_CONFIG_S_FAILED 비트의 저장값이다 (복구용).
- bool config_enabled: 설정(정보) 변경 보고를 활성화할지 여부다.
- bool config_change_pending: 비활성화되어 있을 때, 설정(정보) 변경을 보고할지 여부다.
- spinlock_t config_lock: 설정 변경 보고를 보호하는 스핀락이다.
- spinlock_t vqs_list_lock: vqs(멤버)를 보호하는 스핀락이다.
- struct device dev: 기반 디바이스로, 즉 Linux 디바이스 모델의 struct device 구조체이고, virtio 디바이스를 커널에서 추상화한 것을 표현한다.
- struct virtio_device_id id: 디바이스 타입 식별자다 (이를 드라이버 프로그램과 매칭하는 데 사용).
- const struct virtio_config_ops *config: 이 디바이스의 설정 동작이다.
- const struct vringh_config_ops *vringh_config: 호스트 vrings의 설정 동작이다.
- struct list_head vqs: 이 디바이스의 virtqueues 리스트다.
- u64 features: 드라이버 프로그램과 디바이스가 모두 지원하는 기능이다.
- void *priv: 드라이버 프로그램이 사용하기 위한 전용 포인터다.
(2) virtio_device_id 구조
struct virtio_device_id {
__u32 device;
__u32 vendor;
};
이 구조체는 매우 간단하고 이해하기도 쉬운데, 멤버가 두 개뿐이다.
- device: 디바이스 ID.
- vendor: 벤더(제조사) ID.
그 중 device 멤버는 현재 virtio_device의 용도를 식별한다.
#define VIRTIO_ID_NET 1 /* virtio net */
#define VIRTIO_ID_BLOCK 2 /* virtio block */
#define VIRTIO_ID_CONSOLE 3 /* virtio console */
#define VIRTIO_ID_RNG 4 /* virtio rng */
#define VIRTIO_ID_BALLOON 5 /* virtio balloon */
#define VIRTIO_ID_IOMEM 6 /* virtio ioMemory */
#define VIRTIO_ID_RPMSG 7 /* virtio remote processor messaging */
#define VIRTIO_ID_SCSI 8 /* virtio scsi */
#define VIRTIO_ID_9P 9 /* 9p virtio console */
#define VIRTIO_ID_MAC80211_WLAN 10 /* virtio WLAN MAC */
#define VIRTIO_ID_RPROC_SERIAL 11 /* virtio remoteproc serial link */
#define VIRTIO_ID_CAIF 12 /* Virtio caif */
#define VIRTIO_ID_MEMORY_BALLOON 13 /* virtio memory balloon */
#define VIRTIO_ID_GPU 16 /* virtio GPU */
#define VIRTIO_ID_CLOCK 17 /* virtio clock/timer */
#define VIRTIO_ID_INPUT 18 /* virtio input */
#define VIRTIO_ID_VSOCK 19 /* virtio vsock transport */
#define VIRTIO_ID_CRYPTO 20 /* virtio crypto */
#define VIRTIO_ID_SIGNAL_DIST 21 /* virtio signal distribution device */
#define VIRTIO_ID_PSTORE 22 /* virtio pstore device */
#define VIRTIO_ID_IOMMU 23 /* virtio IOMMU */
#define VIRTIO_ID_MEM 24 /* virtio mem */
#define VIRTIO_ID_SOUND 25 /* virtio sound */
#define VIRTIO_ID_FS 26 /* virtio filesystem */
#define VIRTIO_ID_PMEM 27 /* virtio pmem */
#define VIRTIO_ID_RPMB 28 /* virtio rpmb */
#define VIRTIO_ID_MAC80211_HWSIM 29 /* virtio mac80211-hwsim */
#define VIRTIO_ID_VIDEO_ENCODER 30 /* virtio video encoder */
#define VIRTIO_ID_VIDEO_DECODER 31 /* virtio video decoder */
#define VIRTIO_ID_SCMI 32 /* virtio SCMI */
#define VIRTIO_ID_NITRO_SEC_MOD 33 /* virtio nitro secure module*/
#define VIRTIO_ID_I2C_ADAPTER 34 /* virtio i2c adapter */
#define VIRTIO_ID_WATCHDOG 35 /* virtio watchdog */
#define VIRTIO_ID_CAN 36 /* virtio can */
#define VIRTIO_ID_DMABUF 37 /* virtio dmabuf */
#define VIRTIO_ID_PARAM_SERV 38 /* virtio parameter server */
#define VIRTIO_ID_AUDIO_POLICY 39 /* virtio audio policy */
#define VIRTIO_ID_BT 40 /* virtio bluetooth */
#define VIRTIO_ID_GPIO 41 /* virtio gpio */
우리가 현재 예시로 들고 있는 balloon 디바이스가 바로 그 중 하나다.
#define VIRTIO_ID_BALLOON 5 /* virtio balloon */
(3) virtio_config_ops 구조
struct virtio_config_ops도 앞에서 다룬 적이 있는데, 설정(하나의) virtio 디바이스의 동작들이 들어 있다.
struct virtio_config_ops {
void (*get)(struct virtio_device *vdev, unsigned offset,
void *buf, unsigned len);
void (*set)(struct virtio_device *vdev, unsigned offset,
const void *buf, unsigned len);
u32 (*generation)(struct virtio_device *vdev);
u8 (*get_status)(struct virtio_device *vdev);
void (*set_status)(struct virtio_device *vdev, u8 status);
void (*reset)(struct virtio_device *vdev);
int (*find_vqs)(struct virtio_device *, unsigned nvqs,
struct virtqueue *vqs[], vq_callback_t *callbacks[],
const char * const names[], const bool *ctx,
struct irq_affinity *desc);
void (*del_vqs)(struct virtio_device *);
void (*synchronize_cbs)(struct virtio_device *);
u64 (*get_features)(struct virtio_device *vdev);
int (*finalize_features)(struct virtio_device *vdev);
const char *(*bus_name)(struct virtio_device *vdev);
int (*set_vq_affinity)(struct virtqueue *vq,
const struct cpumask *cpu_mask);
const struct cpumask *(*get_vq_affinity)(struct virtio_device *vdev,
int index);
bool (*get_shm_region)(struct virtio_device *vdev,
struct virtio_shm_region *region, u8 id);
int (*disable_vq_and_reset)(struct virtqueue *vq);
int (*enable_vq_after_reset)(struct virtqueue *vq);
};
struct virtio_config_ops의 각 멤버에 대한 상세 설명은 다음과 같다.
- void (*get)(struct virtio_device *vdev, unsigned offset, void *buf, unsigned len)
콜백 함수(포인터)로, 설정 필드의 값을 읽는다.
파라미터:
vdev: virtio_device.
offset: 설정 필드의 오프셋.
buf: 필드 값을 써넣을 버퍼.
len: 버퍼의 길이.
- void (*set)(struct virtio_device *vdev, unsigned offset, const void *buf, unsigned len)
콜백 함수(포인터)로, 설정 필드에 써넣는다.
파라미터:
vdev: virtio_device.
offset: 설정 필드의 오프셋.
buf: 그 안에서 필드 값을 읽어올 버퍼.
len: 버퍼의 길이.
- u32 (*generation)(struct virtio_device *vdev)
콜백 함수(포인터)로, 설정 생성 카운터다 (옵션).
파라미터:
vdev: virtio_device.
- u8 (*get_status)(struct virtio_device *vdev)
콜백 함수(포인터)로, 상태 바이트를 읽는다.
파라미터:
vdev: virtio_device.
상태 바이트를 반환한다.
- void (*set_status)(struct virtio_device *vdev, u8 status)
콜백 함수(포인터)로, 상태 바이트를 설정한다.
파라미터:
vdev: virtio_device.
status: 새 상태 바이트.
- void (*reset)(struct virtio_device *vdev)
콜백 함수(포인터)로, 디바이스를 리셋한다.
파라미터:
vdev: virtio_device.
디바이스를 리셋한 후에는, 반드시 상태와 기능 협상을 다시 진행해야 한다. 디바이스는 자신의 vq/config 콜백에서 리셋할 수 없고, 추가/삭제와 병행해서 리셋할 수도 없다.
- int (*find_vqs)(struct virtio_device *, unsigned nvqs, struct virtqueue *vqs[], vq_callback_t *callbacks[], const char * const names[], const bool *ctx, struct irq_affinity *desc)
콜백 함수(포인터)로, virtqueues를 찾아서 인스턴스화한다.
파라미터:
vdev: virtio_device.
nvqs: 찾을 virtqueues의 수량.
vqs: 호출 성공 후, 새로운 virtqueues를 포함한다.
callbacks: 콜백 배열로, 모든 virtqueue마다 하나씩 포함하는데, 콜백이 필요 없는 vqs에 대해서는 NULL로 설정한다.
names: virtqueue 이름 배열(주로 디버깅용)로, 드라이버 프로그램이 사용하지 않는 vqs의 NULL 항목을 포함한다.
- void (*del_vqs)(struct virtio_device *)
콜백 함수(포인터)로, find_vqs()로 찾은 사용하지 않는 virtqueues를 해제한다.
파라미터:
vdev: virtio_device.
- void (*synchronize_cbs)(struct virtio_device *)
콜백 함수(포인터)로, virtqueue 콜백과 동기화한다 (옵션).
이 함수는 그 이전의 큐 상의 모든 메모리 동작이, 그 이후에 호출되는 vring_interrupt()에 대해 보일 수 있도록 보장한다.
파라미터:
vdev: virtio_device.
- u64 (*get_features)(struct virtio_device *vdev)
콜백 함수(포인터)로, 해당 디바이스의 특성 비트 배열을 가져온다.
파라미터:
vdev: virtio_device.
앞쪽 64개의 특성 비트를 반환한다 (우리가 현재 필요한 전부다).
- int (*finalize_features)(struct virtio_device *vdev)
콜백 함수(포인터)로, 어떤 디바이스 특성을 사용할지 확정한다.
파라미터:
vdev: virtio_device.
이것은 드라이버 프로그램 특성(특징) 비트를 디바이스로 보낸다: 필요하다면 dev->feature 비트를 변경할 수 있다.
주의, 비록 이 이름으로 불리지만, 임의의 횟수만큼 호출될 수 있다.
성공하면 0을 반환하고, 실패하면 오류 상태를 반환한다.
- const char *(*bus_name)(struct virtio_device *vdev)
콜백 함수(포인터)로, 디바이스와 연관된 버스 이름을 반환한다 (옵션).
파라미터:
vdev: virtio_device.
이것은 버스 이름을 가리키는 포인터를 반환하는데, pci_name을 본뜬 포인터이고, 그러면 호출자는 그 이름에서 복제할 수 있다.
- int (*set_vq_affinity)(struct virtqueue *vq, const struct cpumask *cpu_mask)
콜백 함수(포인터)로, virtqueue의 친화성을 설정한다 (옵션). 즉, 지정된 가상 큐(Virtual Queue)와 특정 CPU 코어 사이의 affinity를 설정한다.
파라미터:
vq: virtqueue.
cpu_mask: CPU 마스크.
- const struct cpumask *(*get_vq_affinity)(struct virtio_device *vdev, int index);
콜백 함수(포인터)로, virtqueue의 affinity 를 가져온다 (옵션). 즉, 지정된 가상 큐(Virtual Queue)와 특정 CPU 코어 사이의 affinity를 가져온다.
파라미터:
vq: virtqueue.
index: 큐의 인덱스 값.
- bool (*get_shm_region)(struct virtio_device *vdev, struct virtio_shm_region *region, u8 id)
콜백 함수(포인터)로, 인덱스에 기반해서 공유 메모리 영역을 가져온다.
파라미터:
vdev: virtio_device.
region: 공유 메모리 영역.
id: 인덱스 값.
- int (*disable_vq_and_reset)(struct virtqueue *vq)
콜백 함수(포인터)로, 큐를 단독으로 리셋한다 (옵션).
파라미터:
vq: virtqueue.
성공하면 0을 반환하고, 실패하면 오류 상태를 반환한다.
disable_vq_and_reset은 콜백이 비활성화되고 동기화되도록 보장한다.
콜백 외에, 호출자는 virtqueue의 어떤 함수도 vring에 접근하지 않도록 보증해야 한다.
- int (*enable_vq_after_reset)(struct virtqueue *vq)
콜백 함수(포인터)로, 리셋된 큐를 활성화한다.
파라미터:
vq: virtqueue.
성공하면 0을 반환하고, 실패하면 오류 상태를 반환한다.
만약 disable_vq_and_reset이 설정되어 있다면, enable_vq_after_reset도 반드시 설정해야 한다.

지금까지 다룬 내용을 큰 흐름으로 정리하면 다음과 같다.

'System Programming > Hypervisor' 카테고리의 다른 글
| QEMU 코드 분석 #9 블록 디바이스 가상화 — QEMU 백엔드 (virtio-blk 클래스 생성 & 디스크 열기) (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 |