1000sj
SJ CODE
1000sj
전체 방문자
오늘
어제
  • 분류 전체보기
    • Algorithms
      • Crypto
      • Formal Methods
    • Security
      • Fuzzing
      • Exploit
    • System Programming
      • Kernel
      • Compiler
      • Device Driver
      • Emulator
      • Assembly
      • Memory
      • Network
      • Hypervisor
    • Architecture
      • ARM
      • RISC-V
    • Cloud Computing
      • Infrastructure
      • SDN
    • TroubleShooting
      • Debugging
      • Testing
    • Performance improvements
      • Parrelel Processing
      • HPC
    • ETC
      • iamroot 스터디
      • 문화 생활
      • 커뮤니티

인기 글

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
1000sj

SJ CODE

System Programming/Hypervisor

QEMU 코드 분석 #1 파라미터 해석 · 모듈 초기화 · QOM · 속성

2024. 3. 25. 00:38

QEMU 명령행 파라미터가 어떻게 파싱되는지, 모듈 초기화(type_init)와 QOM(QEMU Object Model)의 타입 등록·초기화·계층 구조, 객체 생성과 초기화, 속성

 

1) QEMU 파라미터 해석
2) main 함수와 모듈 초기화 (type_init / type_register)
3) QOM — 타입의 등록·초기화·계층
4) QOM — 객체의 생성과 초기화
5) 속성 (Property 와 ObjectProperty)

 

1) QEMU 파라미터 해석 

QEMU 소스코드를 분석하려면, 먼저 QEMU의 command line부터 시작해야 한다. 

$ qemu-system-x86_64 -enable-kvm -m 8G -smp 4 -boot once=d -drive file=./Ubuntu22.img -cdrom ../iso_images/ubuntu...

 

QEMU 명령행 파라미터와 코드 내 구현을 연결 지으며 분석해보았다.

QEMU의 명령어 해석은 `softmmu/vl.c`의 `void qemu_init(int argc, char **argv)` 함수에서 아래와 같이 긴 목록으로 이루어진다

 

qemu_add_opts(&qemu_drive_opts);
qemu_add_drive_opts(&qemu_legacy_drive_opts);
qemu_add_drive_opts(&qemu_common_drive_opts);
qemu_add_drive_opts(&qemu_drive_opts);
qemu_add_drive_opts(&bdrv_runtime_opts);
qemu_add_opts(&qemu_chardev_opts);
qemu_add_opts(&qemu_device_opts);
qemu_add_opts(&qemu_netdev_opts);
qemu_add_opts(&qemu_nic_opts);
qemu_add_opts(&qemu_net_opts);
qemu_add_opts(&qemu_rtc_opts);
qemu_add_opts(&qemu_global_opts);
qemu_add_opts(&qemu_mon_opts);
qemu_add_opts(&qemu_trace_opts);
qemu_plugin_add_opts();
qemu_add_opts(&qemu_option_rom_opts);
qemu_add_opts(&qemu_accel_opts);
qemu_add_opts(&qemu_mem_opts);
qemu_add_opts(&qemu_smp_opts);
qemu_add_opts(&qemu_boot_opts);
qemu_add_opts(&qemu_add_fd_opts);
qemu_add_opts(&qemu_object_opts);
qemu_add_opts(&qemu_tpmdev_opts);
qemu_add_opts(&qemu_overcommit_opts);
qemu_add_opts(&qemu_msg_opts);
qemu_add_opts(&qemu_name_opts);
qemu_add_opts(&qemu_numa_opts);
qemu_add_opts(&qemu_icount_opts);
qemu_add_opts(&qemu_semihosting_config_opts);
qemu_add_opts(&qemu_fw_cfg_opts);
qemu_add_opts(&qemu_action_opts);

 

물론 적지 않은 `qemu_add_opts`가 다른 파일들에 분산되어 있지만, 여기서는 일일이 나열하지 않고 주요 흐름만 잡는다.

 

$ qemu-system-x86_64 -enable-kvm
                     -name instance-00000024
                     -machine pc-i440fx-trusty,accel=kvm,usb=off
                     -cpu SandyBridge,+erms,+smep,+fsgsbase,+pdpe1gb,+rdrand,+f16c,+os...
                     -m 2048
                     -smp 1,sockets=1,cores=1,threads=1
                     ......
                     -rtc base=utc,driftfix=slew
                     -drive file=/var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc...
                     -device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x4,drive=drive-vi...
                     -netdev tap,fd=32,id=hostnet0,vhost=on,vhostfd=37
                     -device virtio-net-pci,netdev=hostnet0,id=net0,mac=fa:16:3e:d1:2d...
                     -chrdev file,id=charserial0,path=/var/lib/nova/instances/1f8e6f7e...
                     -vnc 0.0.0.0:12
                     -device cirrus-vga,id=video0,bus=pci.0,addr=0x2

 

주요 파라미터 설명

  • enable-kvm — 하드웨어 보조 가상화를 활성화한다는 뜻이다.
  • -name instance-00000024 — 가상머신의 이름을 나타낸다.
  • -machine pc-i440fx-trusty,accel=kvm,usb=off — 컴퓨터의 체계 구조(아키텍처)를 나타낸다. KVM 하드웨어 보조 가상화를 사용하면 accel=kvm, 순수 소프트웨어 에뮬레이션이면 accel=tcg 또는 -no-kvm을 사용한다.
  • -cpu SandyBridge,... — CPU 설정. SandyBridge는 Intel 프로세서이고, 뒤의 항목들은 /proc/cpuinfo에 표시되는 추가 CPU 파라미터다.
  • -m 2048 — 사용할 메모리 크기를 나타낸다 (단위: MB).
  • -smp 1,sockets=1,cores=1,threads=1 — SMP(대칭 다중 처리기) 설정. socket은 CPU 소켓 수, core는 코어 수, thread는 하이퍼스레딩이다.
  • -rtc base=utc,driftfix=slew — 시스템 시간 설정.
  • -device cirrus-vga,... — 디스플레이 장치 설정. 기본값은 cirrus(CL-GD5446 PCI VGA 카드 에뮬레이션).
  • -netdev / -device virtio-net-pci — HOST/GUEST 측 네트워크 카드 설정.
  • -drive / -device virtio-blk-pci — HOST/GUEST 측 디스크 설정.
  • -vnc 0.0.0.0:12 — VNC 설정.

 

QEMUOption 구조체와 lookup_opt

QEMU는 QEMUOption을 사용해서 파라미터 옵션을 표현한다. softmmu/vl.c에 정의되어 있다.

typedef struct QEMUOption {
    const char *name;
    int flags;
    int index;
    uint32_t arch_mask;
} QEMUOption;

 

  • name: 파라미터 옵션의 이름
  • flags: 옵션의 속성 (예: 하위 파라미터 유무)
  • arch_mask: 지원하는 체계 구조

전역 배열 qemu_options에 모든 사용 가능한 옵션을 저장한다.

static const QEMUOption qemu_options[] = {
    { "h", 0, QEMU_OPTION_h, QEMU_ARCH_ALL },

#define DEF(option, opt_arg, opt_enum, opt_help, arch_mask)    \
    { option, opt_arg, opt_enum, arch_mask },
#define DEFHEADING(text)
#define ARCHHEADING(text, arch_mask)

    #include "qemu-options.def"
    { /* end of list */ }
};

 

 

main 함수에서는 lookup_opt를 호출해서 QEMU 명령행 파라미터를 해석한다.

static const QEMUOption *lookup_opt(int argc, char **argv,
                                    const char **poptarg, int *poptind)
{
    const QEMUOption *popt;
    int optind = *poptind;
    char *r = argv[optind];
    const char *optarg;

    loc_set_cmdline(argv, optind, 1);
    optind++;

    /* --foo를 -foo와 동일하게 취급 */
    if (r[1] == '-')
        r++;

    popt = qemu_options;
    for(;;) {
        if (!popt->name) {
            error_report("invalid option");
            exit(1);
        }
        if (!strcmp(popt->name, r + 1))
            break;
        popt++;
    }

    if (popt->flags & HAS_ARG) {
        if (optind >= argc) {
            error_report("requires an argument");
            exit(1);
        }
        optarg = argv[optind++];
        loc_set_cmdline(argv, optind - 2, 2);
    } else {
        optarg = NULL;
    }

    *poptarg = optarg;
    *poptind = optind;

    return popt;
}

 

qemu_options 배열에 항목이 별로 없어 보이는데 어떻게 그렇게 많은 파라미터를 해석하는지 의문이 들 수 있다. 비밀은 #include "qemu-options.def" 한 줄에 있다.

이 파일은 QEMU 소스코드를 컴파일한 후에야 생성되며(Makefile에서 scripts/hxtool 스크립트로 qemu-options.hx에서 생성), 총 1000줄이 넘는다. DEF 매크로로 help, version, machine, cpu, accel, smp, numa 등 모든 옵션을 정의한다.

 

QemuOptsList 데이터 구조

QEMUOption은 파라미터의 기본 정보만 제공한다. 실제 파라미터의 저장은 3개의 데이터 구조로 이루어진다.
 
QEMU는 모든 파라미터를 몇 개의 큰 옵션(카테고리)으로 나눈다. 각 큰 옵션은 QemuOptsList 구조체로 표현한다(include/qemu/option.h에 정의).
 
struct QemuOptsList {
    const char *name;
    const char *implied_opt_name;
    bool merge_lists;
    QTAILQ_HEAD(, QemuOpts) head;
    QemuOptDesc desc[];
};

 

전역 변수 vm_config_groups에 최대 48개의 큰 옵션을 저장한다. (util/qemu-config.c)

static QemuOptsList *vm_config_groups[48];
static QemuOptsList *drive_config_groups[5];

 

qemu_add_opts 함수가 각 QemuOptsList를 vm_config_groups에 추가한다.

void qemu_add_opts(QemuOptsList *list)
{
    int entries, i;
    entries = ARRAY_SIZE(vm_config_groups);
    entries--;
    for (i = 0; i < entries; i++) {
        if (vm_config_groups[i] == NULL) {
            vm_config_groups[i] = list;
            return;
        }
    }
    fprintf(stderr, "ran out of space in vm_config_groups");
    abort();
}

 

-overcommit 예시 (고정된 하위 옵션)

static QemuOptsList qemu_overcommit_opts = {
    .name = "overcommit",
    .head = QTAILQ_HEAD_INITIALIZER(qemu_overcommit_opts.head),
    .desc = {
        { .name = "mem-lock", .type = QEMU_OPT_BOOL, },
        { .name = "cpu-pm",   .type = QEMU_OPT_BOOL, },
        { /* end of list */ }
    },
};

 

-device 예시

QemuOptsList qemu_device_opts = {
    .name = "device",
    .implied_opt_name = "driver",
    .head = QTAILQ_HEAD_INITIALIZER(qemu_device_opts.head),
    .desc = {
        /* no elements => accept any */
        { /* end of list */ }
    },
};

 

QemuOpt, QemuOpts 구조체

각 QEMU 하위 옵션은 하나의 QemuOpt 구조체로 표현한다(include/qemu/option_int.h).

struct QemuOpt {
    char *name;
    char *str;
    const QemuOptDesc *desc;
    union {
        bool boolean;
        uint64_t uint;
    } value;
    QemuOpts     *opts;
    QTAILQ_ENTRY(QemuOpt) next;
};

 

QemuOpts 구조체(같은 파일)

struct QemuOpts {
    char *id;
    QemuOptsList *list;
    Location loc;
    QTAILQ_HEAD(, QemuOpt) head;
    QTAILQ_ENTRY(QemuOpts) next;
};

 

  • QemuOptsList (vm_config_groups[48]): 최상위 배열. 각 원소가 하나의 큰 옵션 카테고리.
  • QemuOpts: 같은 카테고리 내에서 여러 인스턴스를 링크드 리스트로 연결.
  • QemuOpt: 각 QemuOpts 아래에 실제 key=value 쌍을 링크드 리스트로 저장.

 

 

-device 파라미터 처리 과정

softmmu/vl.c의 main 함수(qemu_init)에는 약 1000줄짜리 긴 루프가 있어서 파라미터를 해석한다.

-device를 만나면 다음 분기를 탄다.

case QEMU_OPTION_device:
    if (optarg[0] == '{') {
        QObject *obj = qobject_from_json(optarg, &error_fatal);
        DeviceOption *opt = g_new0(DeviceOption, 1);
        opt->opts = qobject_to(QDict, obj);
        loc_save(&opt->loc);
        assert(opt->opts != NULL);
        QTAILQ_INSERT_TAIL(&device_opts, opt, next);
    } else {
        if (!qemu_opts_parse_noisily(qemu_find_opts("device"),
                                     optarg, true)) {
            exit(1);
        }
    }
    break;

 

핵심은 qemu_find_opts("device")로 해당 QemuOptsList를 찾고, qemu_opts_parse_noisily로 파싱하는 것이다.

 

파싱 호출 체인

 

 

2) main 함수와 모듈 초기화

 

QEMU main 함수 분석 — qemu_main

옛날 버전의 main 함수는 softmmu/vl.c에 있었지만, 새로운 버전(qemu-7.1.0)에서는 softmmu/main.c에 분리되었다.

int qemu_main(int argc, char **argv, char **envp)
{
    int status;
    qemu_init(argc, argv, envp);
    status = qemu_main_loop();
    qemu_cleanup();
    return status;
}

#ifndef CONFIG_COCOA
int main(int argc, char **argv)
{
    return qemu_main(argc, argv, NULL);
}
#endif

 

qemu_main 안에는 qemu_init, qemu_main_loop, qemu_cleanup 세 개의 큰 함수가 있다.

 

module_call_init 함수

module_call_init 함수는 util/module.c에 있다.

void module_call_init(module_init_type type)
{
    ModuleTypeList *l;
    ModuleEntry *e;

    if (modules_init_done[type]) {
        return;
    }

    l = find_type(type);

    QTAILQ_FOREACH(e, l, node) {
        e->init();
    }

    modules_init_done[type] = true;
}

 

관련 데이터 구조

MODULE_INIT_OPTS 정의 (include/qemu/module.h)

typedef enum {
    MODULE_INIT_MIGRATION,
    MODULE_INIT_BLOCK,
    MODULE_INIT_OPTS,
    MODULE_INIT_QOM,
    MODULE_INIT_TRACE,
    MODULE_INIT_XEN_BACKEND,
    MODULE_INIT_LIBQOS,
    MODULE_INIT_FUZZ_TARGET,
    MODULE_INIT_MAX
} module_init_type;

 

ModuleEntry 구조 (util/module.c)

typedef struct ModuleEntry
{
    void (*init)(void);
    QTAILQ_ENTRY(ModuleEntry) node;
    module_init_type type;
} ModuleEntry;

 

ModuleTypeList 정의

typedef QTAILQ_HEAD(, ModuleEntry) ModuleTypeList;

전역 변수

static ModuleTypeList init_type_list[MODULE_INIT_MAX];
static bool modules_init_done[MODULE_INIT_MAX];
static ModuleTypeList dso_init_list;

 

type_init 매크로와 모듈 등록 — kvm을 예로

QEMU 모듈을 정의하면 type_init을 호출한다. kvm 예시(accel/kvm/kvm-all.c)

type_init(kvm_type_init);

 

type_init은 매크로(include/qemu/module.h)

#define type_init(function) module_init(function, MODULE_INIT_QOM)

 

module_init 매크로 (BUILD_DSO 미정의 시)

#define module_init(function, type)                             \
static void __attribute__((constructor)) do_qemu_init_ ## function(void)    \
{                                                               \
    register_module_init(function, type);                       \
}

 

type_init(kvm_type_init) 전개 결과

static void __attribute__((constructor)) do_qemu_init_kvm_type_init(void)
{
    register_module_init(kvm_type_init, MODULE_INIT_QOM);
}

 

register_module_init 함수 (util/module.c)

void register_module_init(void (*fn)(void), module_init_type type)
{
    ModuleEntry *e;
    ModuleTypeList *l;

    e = g_malloc0(sizeof(*e));
    e->init = fn;
    e->type = type;

    l = find_type(type);

    QTAILQ_INSERT_TAIL(l, e, node);
}

 

모듈 전체 등록 흐름

 

 

kvm_type_init과 타입 등록

static void kvm_type_init(void)
{
    type_register_static(&kvm_accel_type);
}

 

static const TypeInfo kvm_accel_type = {
    .name = TYPE_KVM_ACCEL,
    .parent = TYPE_ACCEL,
    .instance_init = kvm_accel_instance_init,
    .class_init = kvm_accel_class_init,
    .instance_size = sizeof(KVMState),
};

 

호출 체인은 type_register_static -> type_register -> type_register_internal이다. 

static TypeImpl *type_register_internal(const TypeInfo *info)
{
    TypeImpl *ti;
    ti = type_new(info);
    type_table_add(ti);
    return ti;
}

 

type_register_internal 심층 분석

type_new는 TypeInfo에서 TypeImpl 인스턴스를 생성한다. 전역 해시 테이블 type_table에서 중복 검사 후 새 클래스 정보를 채운다.

type_table_add는 생성된 TypeImpl을 전역 해시 테이블에 등록한다.

static void type_table_add(TypeImpl *ti)
{
    assert(!enumerating_types);
    g_hash_table_insert(type_table_get(), (void *)ti->name, ti);
}

 

static GHashTable *type_table_get(void)
{
    static GHashTable *type_table;
    if (type_table == NULL) {
        type_table = g_hash_table_new(g_str_hash, g_str_equal);
    }
    return type_table;
}

 

이 메커니즘은 Java의 리플렉션과 비슷하다.

 

  • TypeImpl = 종이 위의 클래스 정의
  • class_init → XXXClass = Java의 Class 객체
  • instance_init → Object = 실제 인스턴스 생성

 

3) QOM — 타입의 등록·초기화·계층

QOM 개요

QOM의 전체 이름은 QEMU Object Model이다. C 언어로 상속, 캡슐화, 다형성을 구현했으며 QEMU 코드 전체에 퍼져 있다.

 

타입과 객체의 구분

타입은 종류를 나타내고, 객체는 구체적인 인스턴스다.

예: -device edu,id=edu1, -device edu,id=edu2에서 edu는 타입이고, edu1과 edu2는 객체다.

 

QOM의 세 부분

 

 

 

QOM 타입의 등록 — edu 장치를 예로

hw/misc/edu.c 파일을 예로 들면

static void pci_edu_register_types(void)
{
    static InterfaceInfo interfaces[] = {
        { INTERFACE_CONVENTIONAL_PCI_DEVICE },
        { },
    };
    static const TypeInfo edu_info = {
        .name          = TYPE_PCI_EDU_DEVICE,
        .parent        = TYPE_PCI_DEVICE,
        .instance_size = sizeof(EduState),
        .instance_init = edu_instance_init,
        .class_init    = edu_class_init,
        .interfaces = interfaces,
    };

    type_register_static(&edu_info);
}
type_init(pci_edu_register_types)

 

init_type_list와 ModuleEntry의 관계도

모든 QOM 타입은 main 함수 실행 전에 __attribute__((constructor))를 통해 init_type_list[MODULE_INIT_QOM]에 등록된다.

 

module_call_init 호출 위치

호출 체인: main() → qemu_main() → qemu_init() → qemu_init_subsystems() → module_call_init(MODULE_INIT_QOM)

void qemu_init_subsystems(void)
{
    Error *err = NULL;
    os_set_line_buffering();
    module_call_init(MODULE_INIT_TRACE);
    qemu_init_cpu_list();
    qemu_init_cpu_loop();
    qemu_mutex_lock_iothread();
    atexit(qemu_run_exit_notifiers);
    module_call_init(MODULE_INIT_QOM);
    module_call_init(MODULE_INIT_MIGRATION);
    runstate_init();
    ......
}

 

TypeInfo → TypeImpl 변환

type_register_static → type_register → type_register_internal에서 type_new로 TypeInfo를 TypeImpl로 변환하고, type_table_add로 해시 테이블에 저장한다.

 

TypeImpl 구조체

struct TypeImpl
{
    const char *name;
    size_t class_size;
    size_t instance_size;
    size_t instance_align;

    void (*class_init)(ObjectClass *klass, void *data);
    void (*class_base_init)(ObjectClass *klass, void *data);
    void *class_data;

    void (*instance_init)(Object *obj);
    void (*instance_post_init)(Object *obj);
    void (*instance_finalize)(Object *obj);

    bool abstract;
    const char *parent;
    TypeImpl *parent_type;
    ObjectClass *class;

    int num_interfaces;
    InterfaceImpl interfaces[MAX_INTERFACES];
};

 

  • name — 타입 이름 (예: "edu", "isa-i8259")
  • class_size — 소속 클래스의 크기
  • instance_size — 인스턴스의 크기
  • class_init, class_base_init — 클래스 초기화 함수 (클래스 초기화 시에만 호출)
  • instance_init, instance_post_init, instance_finalize — 인스턴스의 초기화 및 소멸 함수
  • abstract — 추상 타입 여부 (C++의 abstract와 유사)
  • parent — 부모 타입의 이름
  • parent_type — 부모 타입의 TypeImpl
  • class — ObjectClass 포인터
  • interfaces — 인터페이스 정보 (Java의 인터페이스와 유사)

 

QOM 타입의 초기화 — type_initialize

클래스의 초기화는 type_initialize 함수로 이루어진다 (qom/object.c)

static void type_initialize(TypeImpl *ti)
{
    TypeImpl *parent;

    if (ti->class) {
        return;
    }

    // (1) 관련 필드 설정 및 ObjectClass 할당
    ti->class_size = type_class_get_size(ti);
    ti->instance_size = type_object_get_size(ti);
    if (ti->instance_size == 0) {
        ti->abstract = true;
    }
    ti->class = g_malloc0(ti->class_size);

    // (2) 재귀적으로 부모 타입 초기화
    parent = type_get_parent(ti);
    if (parent) {
        type_initialize(parent);
        g_assert(parent->class_size <= ti->class_size);
        g_assert(parent->instance_size <= ti->instance_size);
        memcpy(ti->class, parent->class, parent->class_size);
        ti->class->interfaces = NULL;
        // 부모 인터페이스 및 자체 인터페이스 초기화 ...
    }

    ti->class->properties = g_hash_table_new_full(...);
    ti->class->type = ti;

    // (3) 모든 부모의 class_base_init 및 자신의 class_init 호출
    while (parent) {
        if (parent->class_base_init) {
            parent->class_base_init(ti->class, ti->class_data);
        }
        parent = type_get_parent(parent);
    }

    if (ti->class_init) {
        ti->class_init(ti->class, ti->class_data);
    }
}

 

 

type_initialize 호출 경로

type_initialize는 여러 곳에서 호출될 수 있지만, 첫 번째 호출 때만 실제 초기화가 수행된다 (ti->class가 비어있지 않으면 바로 리턴).

 

QOM 타입의 계층 구조

type_initialize에서 부모 타입도 재귀적으로 초기화하므로, 타입의 계층 구조가 중요하다. QOM은 이를 통해 C++ 같은 상속을 구현했다.

 

edu 장치의 타입 계층

 

 

각 타입의 정의는 다음과 같다.

 

TYPE_PCI_EDU_DEVICE (hw/misc/edu.c)

static const TypeInfo edu_info = {
    .name          = TYPE_PCI_EDU_DEVICE,
    .parent        = TYPE_PCI_DEVICE,
    .instance_size = sizeof(EduState),
    .instance_init = edu_instance_init,
    .class_init    = edu_class_init,
    .interfaces = interfaces,
};

 

TYPE_PCI_DEVICE (hw/pci/pci.c)

static const TypeInfo pci_device_type_info = {
    .name = TYPE_PCI_DEVICE,     // "pci-device"
    .parent = TYPE_DEVICE,
    .instance_size = sizeof(PCIDevice),
    .abstract = true,
    .class_size = sizeof(PCIDeviceClass),
    .class_init = pci_device_class_init,
    .class_base_init = pci_device_class_base_init,
};

 

TYPE_DEVICE (hw/core/qdev.c)

static const TypeInfo device_type_info = {
    .name = TYPE_DEVICE,          // "device"
    .parent = TYPE_OBJECT,
    .instance_size = sizeof(DeviceState),
    .instance_init = device_initfn,
    .instance_post_init = device_post_init,
    .instance_finalize = device_finalize,
    .class_base_init = device_class_base_init,
    .class_init = device_class_init,
    .abstract = true,
    .class_size = sizeof(DeviceClass),
    .interfaces = (InterfaceInfo[]) {
        { TYPE_VMSTATE_IF },
        { TYPE_RESETTABLE_INTERFACE },
        { }
    },
};

 

TYPE_OBJECT (qom/object.c)

static const TypeInfo object_info = {
    .name = TYPE_OBJECT,          // "object"
    .instance_size = sizeof(Object),
    .class_init = object_class_init,
    .abstract = true,
};

 

인터페이스의 계층 구조

모든 interface의 조상은 TYPE_INTERFACE다 (include/qom/object.h)

#define TYPE_INTERFACE "interface"
static const TypeInfo interface_info = {
    .name = TYPE_INTERFACE,
    .class_size = sizeof(InterfaceClass),
    .abstract = true,
};

 

타입의 계층 구조 상세

앞에서 설명했듯이 edu 즉 TYPE_PCI_EDU_DEVICE 타입의 부모 타입은 TYPE_PCI_DEVICE다. 당연히 그 외에도 다른 타입들이 있다.

예를 들어 TYPE_ISA_DEVICE도 마찬가지로 TYPE_DEVICE를 부모 타입으로 하며 ISA 장치를 나타낸다.

 

타입 상속 트리

 

아래에서 데이터 구조 관점에서 타입의 계층 구조를 살펴본다.

 

type_initialize 함수

static void type_initialize(TypeImpl *ti)
{
    TypeImpl *parent;

    if (ti->class) {
        return;
    }

    ti->class_size = type_class_get_size(ti);
    ti->instance_size = type_object_get_size(ti);

    /* Any type with zero instance_size is implicitly abstract.
     * This means interface types are all abstract.
     */
    if (ti->instance_size == 0) {
        ti->abstract = true;
    }
    if (type_is_ancestor(ti, type_interface)) {
        assert(ti->instance_size == 0);
        assert(ti->abstract);
        assert(!ti->instance_init);
        assert(!ti->instance_post_init);
        assert(!ti->instance_finalize);
        assert(!ti->num_interfaces);
    }
    ti->class = g_malloc0(ti->class_size);

    ……

}

타입의 초기화 함수 type_initialize에서 ti->class = g_malloc0(ti->class_size) 구문을 호출해서 타입의 class 구조체를 할당하는데, 이 구조체는 실제로 타입의 정보를 대표한다.

C++이나 Java 같은 객체지향 언어에서 정의하는 클래스와 비슷하다.

앞에서 분석한 것처럼 ti->class_size는 TypeImpl 안의 값인데, 만약 타입 자체에 정의가 없으면 부모 타입의 class_size를 사용해서 초기화한다.

 

edu 장치의 TypeInfo 정의 (hw/misc/edu.c)

static void pci_edu_register_types(void)
{
    static InterfaceInfo interfaces[] = {
        { INTERFACE_CONVENTIONAL_PCI_DEVICE },
        { },
    };
    static const TypeInfo edu_info = {
        .name          = TYPE_PCI_EDU_DEVICE,
        .parent        = TYPE_PCI_DEVICE,
        .instance_size = sizeof(EduState),
        .instance_init = edu_instance_init,
        .class_init    = edu_class_init,
        .interfaces    = interfaces,
    };
    type_register_static(&edu_info);
}
type_init(pci_edu_register_types)

 

edu 장치의 경우, 타입 자체에 class_size 정의가 없다. 그래서 부모 타입인 TYPE_PCI_DEVICE에 정의된 값을 쓰게 되는데, 즉 pci_device_type_info에 있는 class_size 값인 sizeof(PCIDeviceClass)다.

 

PCIDeviceClass 구조체 (include/hw/pci/pci.h)

struct PCIDeviceClass {
    DeviceClass parent_class;

    void (*realize)(PCIDevice *dev, Error **errp);
    PCIUnregisterFunc *exit;
    PCIConfigReadFunc *config_read;
    PCIConfigWriteFunc *config_write;

    uint16_t vendor_id;
    uint16_t device_id;
    uint8_t revision;
    uint16_t class_id;
    uint16_t subsystem_vendor_id;       /* only for header type = 0 */
    uint16_t subsystem_device_id;       /* only for header type = 0 */

    /*
     * pci-to-pci bridge or normal device.
     * This doesn't mean pci host switch.
     * When card bus bridge is supported, this would be enhanced.
     */
    bool is_bridge;

    /* rom bar */
    const char *romfile;
};

PCIDeviceClass는 PCI에 속하는 장치의 정보를 나타낸다.

예를 들어 장치 제조사 정보인 vendor_id, 장치 정보인 device_id, 그리고 PCI 장치 설정 공간을 읽고 쓰는 config_read/config_write 함수 같은 거다.

 

주목할 점은 PCIDeviceClass 구조체의 첫 번째 멤버가 DeviceClass 구조체라는 거다.

이 구조체는 "장치 타입"에 속하는 타입이 가지는 몇 가지 속성을 기술한다. hw/core/qdev.c의 device_type_info에서 볼 수 있다.

 

device_type_info

static const TypeInfo device_type_info = {
    .name = TYPE_DEVICE,
    .parent = TYPE_OBJECT,
    .instance_size = sizeof(DeviceState),
    .instance_init = device_initfn,
    .instance_post_init = device_post_init,
    .instance_finalize = device_finalize,
    .class_base_init = device_class_base_init,
    .class_init = device_class_init,
    .abstract = true,
    .class_size = sizeof(DeviceClass),
    .interfaces = (InterfaceInfo[]) {
        { TYPE_VMSTATE_IF },
        { TYPE_RESETTABLE_INTERFACE },
        { }
    }
};

 

DeviceClass 구조체 (include/hw/qdev-core.h)

struct DeviceClass {
    /*< private >*/
    ObjectClass parent_class;
    /*< public >*/

    DECLARE_BITMAP(categories, DEVICE_CATEGORY_MAX);
    const char *fw_name;
    const char *desc;

    /*
     * The underscore at the end ensures a compile-time error if someone
     * assigns to dc->props instead of using device_class_set_props.
     */
    Property *props_;

    /*
     * Can this device be instantiated with -device / device_add?
     * All devices should support instantiation with device_add, and
     * this flag should not exist.  But we're not there, yet.  Some
     * devices fail to instantiate with cryptic error messages.
     * Others instantiate, but don't work.  Exposing users to such
     * behavior would be cruel; clearing this flag will protect them.
     * It should never be cleared without a comment explaining why it
     * is cleared.
     * TODO remove once we're there
     */
    bool user_creatable;
    bool hotpluggable;

    /* callbacks */
    /*
     * Reset method here is deprecated and replaced by methods in the
     * resettable class interface to implement a multi-phase reset.
     * TODO: remove once every reset callback is unused
     */
    DeviceReset reset;
    DeviceRealize realize;
    DeviceUnrealize unrealize;

    /* device state */
    const VMStateDescription *vmsd;

    /* Private to qdev / bus.  */
    const char *bus_type;
};

DeviceClass는 장치 타입과 관련된 기본 정보랑 기본 콜백 함수들을 정의한다.

역시 주목할 건 첫 번째 필드가 DeviceClass의 부모 타입인 Class — 즉 ObjectClass라는 거다.

전에 말했듯이 ObjectClass는 모든 타입의 기초이고, 해당하는 자식 Class의 첫 번째 필드에 내장된다.

 

Class 구조체의 포함(내장) 관계

ObjectClass, DeviceClass, PCIDeviceClass 세 개의 포함 관계는 아래와 같다

사실 컴파일러가 C++ 상속 구조를 컴파일해서 만들어내는 메모리 분포도 이것과 비슷하다.

 

부모 타입 멤버 필드의 초기화

부모 타입의 멤버 필드는 언제 초기화되느냐? type_initialize 함수부터 다시 봐야 한다. (qom/object.c)

……
ti->class = g_malloc0(ti->class_size);

parent = type_get_parent(ti);
if (parent) {
    type_initialize(parent);
    GSList *e;
    int i;

    g_assert(parent->class_size <= ti->class_size);
    g_assert(parent->instance_size <= ti->instance_size);
    memcpy(ti->class, parent->class, parent->class_size);
    ti->class->interfaces = NULL;
    ……
    ……

바로 memcpy(ti->class, parent->class, parent->class_size);

이 한 줄의 코드가 부모 타입이 차지하는 공간에 대한 초기화를 수행하는 거다.

 

class_init과 타입 변환

다시 클래스의 초기화를 분석해 본다. type_initialize 함수의 마지막 코드는

……
if (ti->class_init) {
    ti->class_init(ti->class, ti->class_data);
}

첫 번째 인자가 ti->class인데, edu의 경우 방금 할당한 PCIDeviceClass다.

그런데 class_init 콜백의 인자 지정 타입은 ObjectClass이기 때문에, ObjectClass에서 PCIDeviceClass로의 타입 변환이 필요하다. hw/misc/edu.c의 edu_class_init 함수에서 이 변환을 볼 수 있다.

static void edu_class_init(ObjectClass *class, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(class);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(class);

    k->realize = pci_edu_realize;
    k->exit = pci_edu_uninit;
    k->vendor_id = PCI_VENDOR_ID_QEMU;
    k->device_id = 0x11e8;
    k->revision = 0x10;
    k->class_id = PCI_CLASS_OTHERS;
    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}

 

object_class_dynamic_cast 함수

타입 변환은 PCI_DEVICE_CLASS로 수행된다 (물론 DEVICE_CLASS도 변환을 완료한다).

이 매크로는 여러 겹의 확장을 거쳐 최종적으로 object_class_dynamic_cast 함수를 호출하게 된다. 이 함수는 qom/object.c에 있다.

ObjectClass *object_class_dynamic_cast(ObjectClass *class,
                                       const char *typename)
{
    ObjectClass *ret = NULL;
    TypeImpl *target_type;
    TypeImpl *type;

    if (!class) {
        return NULL;
    }

    /* A simple fast path that can trigger a lot for leaf classes.  */
    type = class->type;
    if (type->name == typename) {
        return class;
    }

    target_type = type_get_by_name(typename);
    if (!target_type) {
        /* target class type unknown, so fail the cast */
        return NULL;
    }

    if (type->class->interfaces &&
            type_is_ancestor(target_type, type_interface)) {
        int found = 0;
        GSList *i;

        for (i = class->interfaces; i; i = i->next) {
            ObjectClass *target_class = i->data;

            if (type_is_ancestor(target_class->type, target_type)) {
                ret = target_class;
                found++;
            }
        }

        /* The match was ambiguous, don't allow a cast */
        if (found > 1) {
            ret = NULL;
        }
    } else if (type_is_ancestor(type, target_type)) {
        ret = class;
    }

    return ret;
}

함수 이름 object_class_dynamic_cast에서 알 수 있듯이, 이건 동적 타입 변환이다.

C++에도 비슷한 dynamic_cast가 있어서 부모 클래스에서 자식 클래스로의 변환 작업을 수행한다. object_class_dynamic_cast 함수의 첫 번째 인자는 변환할 ObjectClass이고, 두 번째 인자 typename은 어떤 타입으로 변환할지를 나타낸다.

 

object_class_dynamic_cast는 먼저 type_get_by_name 함수를 통해 변환할 대상의 TypeImpl을 얻는다. 여기서 typename은 TYPE_PCI_DEVICE다. 

/* A simple fast path that can trigger a lot for leaf classes.  */
type = class->type;
if (type->name == typename) {
    return class;
}

target_type = type_get_by_name(typename);
if (!target_type) {
    /* target class type unknown, so fail the cast */
    return NULL;
}

 

edu를 예로 들면, type의 ->name은 "edu"인데, 변환하려는 건 TYPE_PCI_DEVICE다. 즉 include/hw/pci/pci.h에 정의된 "pci-device"다.

#define TYPE_PCI_DEVICE "pci-device"

따라서 type_is_ancestor("edu", TYPE_PCI_DEVICE)를 호출해서 후자가 전자의 조상인지 판단한다.

 

이 함수에서 순차적으로 edu의 부모 타입을 얻고, TYPE_PCI_DEVICE와 같은지 판단하는데, edu 장치의 TypeInfo에서 부모 타입이 TYPE_PCI_DEVICE인 걸 알 수 있으니까 이 type_is_ancestor는 성공한다.

 

이렇게 ObjectClass에서 PCIDeviceClass로의 변환이 가능해진다.

(PCIDeviceClass *)ObjectClass로 직접 강제 변환을 수행하는 거다.

 

동적 타입 변환 흐름

 

4) QOM — 객체의 생성과 초기화

객체의 구조와 초기화

앞의 두 부분을 되짚어 본다.
첫째, 각 타입은 하나의 TypeInfo를 지정해서 시스템에 등록하고, 시스템이 초기화 시 TypeInfo를 TypeImpl로 변환해서 해시 테이블에 넣는다. 이것이 타입의 등록이다.

 

둘째, 시스템은 해시 테이블의 각 타입을 초기화하는데, 주로 TypeImpl의 필드를 설정하고 class_init 함수를 호출한다. 이것이 타입의 초기화다.

 

이제 모든 타입의 정보가 있고 초기화 함수도 호출됐으니, 필요에 따라(명령줄에서 지정한 인자처럼) 각 타입에 대응하는 객체를 만들어야 한다.

 

객체 생성 호출 체인

아래에서 -device edu 명령을 분석해 본다. (softmmu/vl.c의 qemu_create_cli_devices 함수)

qemu_opts_foreach(qemu_find_opts("device"),
                  device_init_func, NULL, &error_fatal);

 

여기서는 QEMU 인자 구성 같은 객체 구조와 크게 관련 없는 세부 사항은 생략하고, 객체의 구조에만 집중한다.

 

device_init_func

각 -device 인자에 대해 device_init_func 함수를 호출한다. ( softmmu/vl.c)

static int device_init_func(void *opaque, QemuOpts *opts, Error **errp)
{
    DeviceState *dev;

    dev = qdev_device_add(opts, errp);
    if (!dev && *errp) {
        error_report_err(*errp);
        return -1;
    } else if (dev) {
        object_unref(OBJECT(dev));
    }
    return 0;
}

보면 알겠지만, device_init_func 함수는 바로 qdev_device_add 함수를 호출해서 장치를 추가한다.

 

qdev_device_add (softmmu/qdev-monitor.c)

/* Takes ownership of @opts on success */
DeviceState *qdev_device_add(QemuOpts *opts, Error **errp)
{
    QDict *qdict = qemu_opts_to_qdict(opts, NULL);
    DeviceState *ret;

    ret = qdev_device_add_from_qdict(qdict, false, errp);
    if (ret) {
        qemu_opts_del(opts);
    }
    qobject_unref(qdict);
    return ret;
}

qdev_device_add_from_qdict는 같은 파일에 있는데, 전체 코드가 좀 길어서 여기서는 안 붙이고, 그 안에서 qdev_new 함수를 호출해서 장치를 생성하는 부분만 본다.

DeviceState *qdev_device_add_from_qdict(const QDict *opts,
                                        bool from_json, Error **errp)
{
    ……
    /* create device */
    dev = qdev_new(driver);
    ……
}

 

qdev_new (hw/core/qdev.c)

DeviceState *qdev_new(const char *name)
{
    if (!object_class_by_name(name)) {
        module_load_qom_one(name);
    }
    return DEVICE(object_new(name));
}

 

object_new (qom/object.c)

최종적으로 object_new 함수를 통해 객체를 구성한다.

Object *object_new(const char *typename)
{
    TypeImpl *ti = type_get_by_name(typename);

    return object_new_with_type(ti);
}

 

object_new_with_type (qom/object.c)

static Object *object_new_with_type(Type type)
{
    Object *obj;
    size_t size, align;
    void (*obj_free)(void *);

    g_assert(type != NULL);
    type_initialize(type);

    size = type->instance_size;
    align = type->instance_align;

    /*
     * Do not use qemu_memalign unless required.  Depending on the
     * implementation, extra alignment implies extra overhead.
     */
    if (likely(align <= __alignof__(qemu_max_align_t))) {
        obj = g_malloc(size);
        obj_free = g_free;
    } else {
        obj = qemu_memalign(align, size);
        obj_free = qemu_vfree;
    }

    object_initialize_with_type(obj, size, type);
    obj->free = obj_free;

    return obj;
}

 

object_initialize_with_type (qom/object.c)

static void object_initialize_with_type(Object *obj, size_t size, TypeImpl *type)
{
    type_initialize(type);

    g_assert(type->instance_size >= sizeof(Object));
    g_assert(type->abstract == false);
    g_assert(size >= type->instance_size);

    memset(obj, 0, type->instance_size);
    obj->class = type->class;
    object_ref(obj);
    object_class_property_init_all(obj);
    obj->properties = g_hash_table_new_full(g_str_hash, g_str_equal,
                                            NULL, object_property_free);
    object_init_with_type(obj, type);
    object_post_init_with_type(obj, type);
}

이렇게 여러 겹의 호출을 거쳐 최종적으로 object_init_with_type 함수에 도달한다.

 

object_init_with_type 심층 분석

device_init_func 함수가 여러 겹의 호출을 거쳐 최종적으로 object_initialize_with_type 함수에 도달했다고 했다.

device_init_func -> qdev_device_add -> qdev_device_add_from_qdict -> qdev_new ->
object_new -> object_new_with_type -> object_initialize_with_type -> object_init_with_type

 

이번에는 object_init_with_type 함수를 깊이 파고든다.

 

object_init_with_type (qom/object.c)

static void object_init_with_type(Object *obj, TypeImpl *ti)
{
    if (type_has_parent(ti)) {
        object_init_with_type(obj, type_get_parent(ti));
    }

    if (ti->instance_init) {
        ti->instance_init(obj);
    }
}

전자 (object_init_with_type 함수)는 재귀를 통해 모든 부모 타입의 객체 초기화 함수와 자기 자신의 초기화 함수를 호출한다.

 

object_post_init_with_type (qom/object.c)

object_post_init_with_type 함수는 TypeImpl의 instance_post_init 콜백 멤버를 호출해서 객체 초기화 이후의 작업을 완료한다.

static void object_post_init_with_type(Object *obj, TypeImpl *ti)
{
    if (ti->instance_post_init) {
        ti->instance_post_init(obj);
    }

    if (type_has_parent(ti)) {
        object_post_init_with_type(obj, type_get_parent(ti));
    }
}

 

재귀 호출 순서 비교

두 함수의 재귀 호출 순서가 다르다는 점에 주목한다.

 

심층 분석

지금까지 QEMU 객체의 구조와 초기화의 함수 호출 흐름을 분석했는데, 이제 실제 예제를 통해 이 흐름을 깊이 소개하고 분석한다.

 

이전의 edu TypeInfo를 예로 든다.

edu의 객체 크기는 sizeof(EduState)다. 따라서 실제로 하나의 edu 타입의 객체는 EduState 구조체이고, 모든 객체는 각각 하나의 XXXState와 대응한다.

이건 해당 객체의 관련 정보를 기록한다. edu가 PCI 장치라면, EduState에는 이 장치의 몇 가지 정보가 들어 있다.

예를 들어 중단 정보, 장치 상태, 사용하는 MMIO와 PIO에 대응하는 메모리 영역 등이다.

 

EduState 구조체 (hw/misc/edu.c)

struct EduState {
    PCIDevice pdev;
    MemoryRegion mmio;

    QemuThread thread;
    QemuMutex thr_mutex;
    QemuCond thr_cond;
    bool stopping;

    uint32_t addr4;
    uint32_t fact;
#define EDU_STATUS_COMPUTING    0x01
#define EDU_STATUS_IRQFACT      0x80
    uint32_t status;

    uint32_t irq_status;

#define EDU_DMA_RUN             0x1
#define EDU_DMA_DIR(cmd)        (((cmd) & 0x2) >> 1)
# define EDU_DMA_FROM_PCI       0
# define EDU_DMA_TO_PCI         1
#define EDU_DMA_IRQ             0x4
    struct dma_state {
        dma_addr_t src;
        dma_addr_t dst;
        dma_addr_t cnt;
        dma_addr_t cmd;
    } dma;
    QEMUTimer dma_timer;
    char dma_buf[DMA_SIZE];
    uint64_t dma_mask;
};

첫 번째 멤버의 타입에 주목한다: PCIDevice 구조체다.

 

PCIDevice 구조체

이건 pci_device_type_info에 대응한다. 코드는 아래와 같다 (hw/pci/pci.c)

static const TypeInfo pci_device_type_info = {
    .name = TYPE_PCI_DEVICE,
    .parent = TYPE_DEVICE,
    .instance_size = sizeof(PCIDevice),
    .abstract = true,
    .class_size = sizeof(PCIDeviceClass),
    .class_init = pci_device_class_init,
    .class_base_init = pci_device_class_base_init,
};

 

PCIDevice 구조체는 include/hw/pci/pci.h에 정의되어 있다.

struct PCIDevice {
    DeviceState qdev;
    bool partially_hotplugged;
    bool has_power;

    /* PCI config space */
    uint8_t *config;

    /* Used to enable config checks on load. Note that writable bits are
     * never checked even if set in cmask. */
    uint8_t *cmask;

    /* Used to implement R/W bytes */
    uint8_t *wmask;

    /* Used to implement RW1C(Write 1 to Clear) bytes */
    uint8_t *w1cmask;

    /* Used to allocate config space for capabilities. */
    uint8_t *used;

    /* the following fields are read only */
    int32_t devfn;
    /* Cached device to fetch requester ID from, to avoid the PCI
     * tree walking every time we invoke PCI request (e.g.,
     * MSI). For conventional PCI root complex, this field is
     * meaningless. */
    PCIReqIDCache requester_id_cache;
    char name[64];
    PCIIORegion io_regions[PCI_NUM_REGIONS];
    AddressSpace bus_master_as;
    MemoryRegion bus_master_container_region;
    MemoryRegion bus_master_enable_region;

    /* do not access the following fields */
    PCIConfigReadFunc *config_read;
    PCIConfigWriteFunc *config_write;

    /* Legacy PCI VGA regions */
    MemoryRegion *vga_regions[QEMU_PCI_VGA_NUM_REGIONS];
    bool has_vga;

    /* Current IRQ levels.  Used internally by the generic PCI code. */
    uint8_t irq_state;

    /* Capability bits */
    uint32_t cap_present;

    /* Offset of MSI-X capability in config space */
    uint8_t msix_cap;

    /* MSI-X entries */
    int msix_entries_nr;

    /* Space to store MSIX table & pending bit array */
    uint8_t *msix_table;
    uint8_t *msix_pba;

    /* May be used by INTx or MSI during interrupt notification */
    void *irq_opaque;

    MSITriggerFunc *msi_trigger;
    MSIPrepareMessageFunc *msi_prepare_message;
    MSIxPrepareMessageFunc *msix_prepare_message;

    /* MemoryRegion container for msix exclusive BAR setup */
    MemoryRegion msix_exclusive_bar;
    /* Memory Regions for MSIX table and pending bit entries. */
    MemoryRegion msix_table_mmio;
    MemoryRegion msix_pba_mmio;

    /* Reference-count for entries actually in use by driver. */
    unsigned *msix_entry_used;
    /* MSIX function mask set or MSIX disabled */
    bool msix_function_masked;
    /* Version id needed for VMState */
    int32_t version_id;

    /* Offset of MSI capability in config space */
    uint8_t msi_cap;

    /* PCI Express */
    PCIExpressDevice exp;

    /* SHPC */
    SHPCDevice *shpc;

    /* Location of option rom */
    char *romfile;
    uint32_t romsize;
    bool has_rom;
    MemoryRegion rom;
    uint32_t rom_bar;

    /* INTx routing notifier */
    PCIINTxRoutingNotifier intx_routing_notifier;

    /* MSI-X notifiers */
    MSIVectorUseNotifier msix_vector_use_notifier;
    MSIVectorReleaseNotifier msix_vector_release_notifier;
    MSIVectorPollNotifier msix_vector_poll_notifier;

    /* ID of standby device in net_failover pair */
    char *failover_pair_id;
    uint32_t acpi_index;
};

 

DeviceState 구조체

마찬가지로 첫 번째 멤버의 타입에 주목한다: DeviceState 구조체다.

/**
 * DeviceState:
 * @realized: Indicates whether the device has been fully constructed.
 *            When accessed outside big qemu lock, must be accessed with
 *            qatomic_load_acquire()
 * @reset: ResettableState for the device; handled by Resettable interface.
 *
 * This structure should not be accessed directly.  We declare it here
 * so that it can be embedded in individual device state structures.
 */
struct DeviceState {
    /*< private >*/
    Object parent_obj;
    /*< public >*/

    char *id;
    char *canonical_path;
    bool realized;
    bool pending_deleted_event;
    int64_t pending_deleted_expires_ms;
    QDict *opts;
    int hotpluggable;
    bool allow_unplug_during_migration;
    BusState *parent_bus;
    QLIST_HEAD(, NamedGPIOList) gpios;
    QLIST_HEAD(, NamedClockList) clocks;
    QLIST_HEAD(, BusState) child_bus;
    int num_child_bus;
    int instance_id_alias;
    int alias_required_for_version;
    ResettableState reset;
    GSList *unplug_blockers;
};

 

객체(State) 구조체의 포함(내장) 관계

Class 구조체와 마찬가지로, 객체(State) 구조체에서도 첫 번째 멤버가 부모 타입의 State다.

edu_info, pci_device_type_info, device_type_info와 그에 대응하는 EduState, PCIDevice, DeviceState의 정의를 보면 알 수 있듯이, 객체 사이에도 사실상 부모 객체와 자식 객체의 관계가 존재한다.

타입과 마찬가지로, QOM의 객체도 매크로를 사용해서 Object 객체의 포인터를 자식 타입 객체의 포인터로 변환할 수 있다. 변환 과정은 ObjectClass와 비슷하다.

 

여기서 알 수 있는 건, 타입과 달리 object는 필요에 따라 생성되는 거라는 점이다. 명령줄에서 장치를 지정하거나 핫플러그로 장치를 꽂은 다음에야 해당 object가 생성된다. 타입과 객체 사이의 관계는 Object의 Class 필드를 통해 연결된다. 이건 object_initialize_with_type 함수에서 obj->class = type->class를 통해 구현된다.

 

Class와 Object의 연결 관계

 

Java 리플렉션에 비유

앞의 내용을 종합하면 QOM의 객체 구조는 세 부분으로 나눌 수 있다

 

첫 번째 부분은 타입의 구조, TypeInfo를 통해 TypeImpl의 해시 테이블을 구성하는 것이고, 이건 main 함수 이전에 완료된다.

두 번째 부분은 타입의 초기화인데, 이 두 부분은 모두 전역적이다. 즉 컴파일해서 넣은 모든 QOM 객체는 다 조회할 수 있다.

세 번째 부분은 타입의 객체 구조인데, 이건 구체적인 객체 인스턴스를 만드는 거다. 명령줄에서 해당 장치를 지정했을 때만 객체가 생성된다.

 

Java의 리플렉션 메커니즘에 비유하자면: Java에서 하나의 클래스에 대해, 먼저 코드를 작성할 때 class xxx의 정의를 쓰는데, 컴파일하면 .class 파일에 들어간다. 이 부분은 위의 첫 번째 부분 — 타입의 구조에 대응한다.

그 다음 Java는 하나의 Class 객체를 가지는데, 이 종이 위의 클래스를 읽어서 표현하는 데 쓴다. 이 부분은 위의 두 번째 부분 — 타입의 초기화에 대응한다.

마지막으로 Java가 진짜 객체를 생성하는데, 이 부분은 위의 세 번째 부분 — 객체의 구조에 대응한다.

 

실제 흐름으로 다시 설명하면: class_init 함수가 XXXClass를 생성하는데, 이건 Java의 Class 객체에 해당한다. TypeImpl에는 또 instance_init 함수가 있는데, 이건 생성자 함수에 해당하고 XXXClass를 기반으로 Object를 생성하는 데 쓰인다. 이게 바로 Java 리플렉션에서 최종적으로 만들어지는 객체에 해당한다.

 

5) 속성 (Property 와 ObjectProperty)

속성 (Property)

앞에서 QEMU QOM의 세 부분을 차례로 다뤘다.

타입의 구조 (타입의 등록), 타입의 초기화, 객체의 구조 (객체의 초기화)다.

과정은 복잡하지만, 여기까지 왔으면 사실 객체를 구성해서 객체의 초기화 함수를 호출한 것뿐이다.

 

하지만 EduState의 데이터는 아직 채워지지 않았고, 이 시점에서 edu 장치는 사용할 수 없는 상태다.

장치에 대해서 말하면, realized 속성을 true (참)로 설정해야 한다. 

QOM은 C++의 클래스 기반 다형성과 비슷한 걸 구현했다. 하나의 객체는 상속 체계에 따라 Object, DeviceState, PCIDevice 등등이 될 수 있다.

QOM에서는 편리한 관리를 위해, 각 타입과 객체에 속성을 추가했다. 타입 속성은 ObjectClass의 properties 필드에 존재하는데, 이 필드는 타입 초기화 함수 type_initialize에서 구성된 거다.

 

ObjectClass 구조체

이해를 위해 include/qom/object.h의 ObjectClass 타입 정의를 다시 붙인다.

/**
 * struct ObjectClass:
 *
 * The base for all classes.  The only thing that #ObjectClass contains is an
 * integer type handle.
 */
struct ObjectClass
{
    /* private: */
    Type type;
    GSList *interfaces;

    const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE];
    const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE];

    ObjectUnparent *unparent;

    GHashTable *properties;
};

 

TypeImpl 구조체 (qom/object.c)

struct TypeImpl
{
    const char *name;

    size_t class_size;

    size_t instance_size;
    size_t instance_align;

    void (*class_init)(ObjectClass *klass, void *data);
    void (*class_base_init)(ObjectClass *klass, void *data);

    void *class_data;

    void (*instance_init)(Object *obj);
    void (*instance_post_init)(Object *obj);
    void (*instance_finalize)(Object *obj);

    bool abstract;

    const char *parent;
    TypeImpl *parent_type;

    ObjectClass *class;

    int num_interfaces;
    InterfaceImpl interfaces[MAX_INTERFACES];
};

 

type_initialize 함수 ( qom/object.c )

static void type_initialize(TypeImpl *ti)
{
    TypeImpl *parent;

    if (ti->class) {
        return;
    }

    ti->class_size = type_class_get_size(ti);
    ti->instance_size = type_object_get_size(ti);
    /* Any type with zero instance_size is implicitly abstract.
     * This means interface types are all abstract.
     */
    if (ti->instance_size == 0) {
        ti->abstract = true;
    }
    if (type_is_ancestor(ti, type_interface)) {
        assert(ti->instance_size == 0);
        assert(ti->abstract);
        assert(!ti->instance_init);
        assert(!ti->instance_post_init);
        assert(!ti->instance_finalize);
        assert(!ti->num_interfaces);
    }
    ti->class = g_malloc0(ti->class_size);

    parent = type_get_parent(ti);
    if (parent) {
        type_initialize(parent);
        GSList *e;
        int i;

        g_assert(parent->class_size <= ti->class_size);
        g_assert(parent->instance_size <= ti->instance_size);
        memcpy(ti->class, parent->class, parent->class_size);
        ti->class->interfaces = NULL;

        for (e = parent->class->interfaces; e; e = e->next) {
            InterfaceClass *iface = e->data;
            ObjectClass *klass = OBJECT_CLASS(iface);

            type_initialize_interface(ti, iface->interface_type, klass->type);
        }

        for (i = 0; i < ti->num_interfaces; i++) {
            TypeImpl *t = type_get_by_name(ti->interfaces[i].typename);
            if (!t) {
                error_report("missing interface '%s' for object '%s'",
                             ti->interfaces[i].typename, parent->name);
                abort();
            }
            for (e = ti->class->interfaces; e; e = e->next) {
                TypeImpl *target_type = OBJECT_CLASS(e->data)->type;

                if (type_is_ancestor(target_type, t)) {
                    break;
                }
            }

            if (e) {
                continue;
            }

            type_initialize_interface(ti, t, t);
        }

        ti->class->properties = g_hash_table_new_full(g_str_hash, g_str_equal,
                                                      NULL, object_property_free);

        ti->class->type = ti;

        while (parent) {
            if (parent->class_base_init) {
                parent->class_base_init(ti->class, ti->class_data);
            }
            parent = type_get_parent(parent);
        }

        if (ti->class_init) {
            ti->class_init(ti->class, ti->class_data);
        }
    }
}

 

Object 구조체 (include/qom/object.h)

객체 속성은 Object의 properties 필드에 저장되는데, 이 필드는 객체의 초기화 함수 object_initialize_with_type에서 구성된다.

/**
 * struct Object:
 *
 * The base for all objects.  The first member of this object is a pointer to
 * a #ObjectClass.  Since C guarantees that the first member of a structure
 * always begins at byte 0 of that structure, as long as any sub-object places
 * its parent as the first member, we can cast directly to a #Object.
 *
 * As a result, #Object contains a reference to the objects type as its
 * first member.  This allows identification of the real type of the object at
 * run time.
 */
struct Object
{
    /* private: */
    ObjectClass *class;
    ObjectFree *free;
    GHashTable *properties;
    uint32_t ref;
    Object *parent;
};

 

타입 속성과 객체 속성

타입의 속성과 객체의 속성은 모두 GHashTable 타입이다. 즉 둘 다 해시 테이블이고, 테이블 안에는 속성 이름에서 ObjectProperty로의 매핑이 저장되어 있다.

 

ObjectProperty 심층 분석 (include/qom/object.h)

typedef struct ObjectProperty ObjectProperty;

......

struct ObjectProperty
{
    char *name;
    char *type;
    char *description;
    ObjectPropertyAccessor *get;
    ObjectPropertyAccessor *set;
    ObjectPropertyResolve *resolve;
    ObjectPropertyRelease *release;
    ObjectPropertyInit *init;
    void *opaque;
    QObject *defval;
};

 

  • name: 표(테이블) 이름이다.
  • type: 속성의 타입을 나타낸다. 어떤 속성은 문자열이고, 어떤 건 bool 타입이고, 어떤 건 link 같은 더 복잡한 타입이다.
  • description: 속성에 대한 설명을 나타낸다.
  • get: 속성을 가져오는 데 쓴다.
  • set: 속성을 설정하는 데 쓴다.
  • resolve: 속성에 대한 조작을 한다.
  • release: 속성을 해제한다.
  • opaque: 구체적인 속성을 가리킨다. 예를 들면 BoolProperty 같은 것이다.

각 구체적인 속성마다 하나의 구조체가 있어서 그걸 기술한다.

예를 들어: 불리언 타입의 속성은 BoolProperty, 문자열 타입의 속성은 StringProperty, 그리고 link 타입의 속성은 LinkProperty이다. 셋 다 qom/object.c에 정의되어 있고, 정의는 각각 다음과 같다.

 

typedef struct {
    union {
        Object **targetp;
        Object *target; /* if OBJ_PROP_LINK_DIRECT, when holding the pointer  */
        ptrdiff_t offset; /* if OBJ_PROP_LINK_CLASS */
    };
    void (*check)(const Object *, const char *, Object *, Error **);
    ObjectPropertyLinkFlags flags;
} LinkProperty;

......

typedef struct StringProperty
{
    char *(*get)(Object *, Error **);
    void (*set)(Object *, const char *, Error **);
} StringProperty;

......

typedef struct BoolProperty
{
    bool (*get)(Object *, Error **);
    void (*set)(Object *, bool, Error **);
} BoolProperty;

 

Object, ObjectProperty, 구체적 속성의 관계

Object, ObjectProperty와 각 구체적 속성의 관계를 정리하면 다음과 같다.

  • Object (struct object) -> GHashTable *properties에 속성 이름부터 ObjectProperty (struct ObjectProperty)까지의 매핑이 저장되어 있다.
  • ObjectProperty -> void *opaque가 하나의 구체적인 속성을 가리킨다.

 

속성 관련 인터페이스

아래에서 몇 가지 속성 관련 인터페이스를 소개한다. 속성의 추가는 클래스 속성의 추가와 객체 속성의 추가로 나뉜다.

 

객체 속성의 추가

객체 속성의 경우, 속성 추가는 object_property_add 인터페이스를 통해 완성된다. object_property_add 함수는 qom/object.c에 있고, 코드는 다음과 같다

ObjectProperty *
object_property_add(Object *obj, const char *name, const char *type,
                    ObjectPropertyAccessor *get,
                    ObjectPropertyAccessor *set,
                    ObjectPropertyRelease *release,
                    void *opaque)
{
    return object_property_try_add(obj, name, type, get, set, release,
                                  opaque, &error_abort);
}

 

이 함수는 실제로 object_property_try_add 함수의 간단한 래핑이다. object_property_try_add 함수는 바로 위에 있고, 코드는 다음과 같다.

ObjectProperty *
object_property_try_add(Object *obj, const char *name, const char *type,
                        ObjectPropertyAccessor *get,
                        ObjectPropertyAccessor *set,
                        ObjectPropertyRelease *release,
                        void *opaque, Error **errp)
{
    ObjectProperty *prop;
    size_t name_len = strlen(name);

    if (name_len >= 3 && !memcmp(name + name_len - 3, "[*]", 4)) {
        int i;
        ObjectProperty *ret = NULL;
        char *name_no_array = g_strdup(name);

        name_no_array[name_len - 3] = '\0';
        for (i = 0; i < INT16_MAX; ++i) {
            char *full_name = g_strdup_printf("%s[%d]", name_no_array, i);

            ret = object_property_try_add(obj, full_name, type, get, set,
                                          release, opaque, NULL);
            g_free(full_name);
            if (ret) {
                break;
            }
        }
        g_free(name_no_array);
        assert(ret);
        return ret;
    }

    if (object_property_find(obj, name) != NULL) {
        error_setg(errp, "attempt to add duplicate property '%s' to object (type '%s')",
                   name, object_get_typename(obj));
        return NULL;
    }

    prop = g_malloc0(sizeof(*prop));

    prop->name = g_strdup(name);
    prop->type = g_strdup(type);

    prop->get = get;
    prop->set = set;
    prop->release = release;
    prop->opaque = opaque;

    g_hash_table_insert(obj->properties, prop->name, prop);

    return prop;
}

일단 와일드카드의 경우, 즉 if (name_len >= 3 && !memcmp(name + name_len - 3, "[*]", 4)) 조건 판단 안의 내용은 건너뛰고 아래를 보자.

먼저 object_property_find 함수를 호출해서 삽입하려는 속성이 이미 존재하는지 확인한다. 이미 존재하면 에러를 리턴하고, 중복 속성이 추가되지 않도록 한다.

그 다음 하나의 ObjectProperty 구조체를 할당하고 함수 인자를 사용해서 초기화한다. 코드 조각은 다음과 같다.

    prop = g_malloc0(sizeof(*prop));

    prop->name = g_strdup(name);
    prop->type = g_strdup(type);

    prop->get = get;
    prop->set = set;
    prop->release = release;
    prop->opaque = opaque;

마지막으로, 아래 코드를 호출해서 객체의 properties 필드에 삽입하고, 이 prop을 리턴한다.

g_hash_table_insert(obj->properties, prop->name, prop);

 

object_property_try_add 호출 흐름

 

object_property_find 함수

돌아와서, object_property_find 함수의 구체적인 구현을 보자. object_property_find 함수는 같은 파일 (qom/object.c)에 있고, 코드는 다음과 같다.

ObjectProperty *object_property_find(Object *obj, const char *name)
{
    ObjectProperty *prop;
    ObjectClass *klass = object_get_class(obj);

    prop = object_class_property_find(klass, name);
    if (prop) {
        return prop;
    }

    return g_hash_table_lookup(obj->properties, name);
}

 

이 함수는 먼저 object_get_class 함수를 호출해서 Object 구조체 객체(의 포인터) obj로부터 ObjectClass 객체(의 포인터) klass를 얻는다. object_get_class 함수는 qom/object.c에 있고, 코드는 다음과 같다.

ObjectClass *object_get_class(Object *obj)
{
    return obj->class;
}

 

그런 다음, object_class_property_find 함수를 호출해서 자기가 속한 클래스와 모든 부모 클래스에 이 속성이 없는지 확인한다. object_class_property_find 함수도 마찬가지로 qom/object.c에 있고, 코드는 다음과 같다.

ObjectProperty *object_class_property_find(ObjectClass *klass, const char *name)
{
    ObjectClass *parent_klass;

    parent_klass = object_class_get_parent(klass);
    if (parent_klass) {
        ObjectProperty *prop =
            object_class_property_find(parent_klass, name);
        if (prop) {
            return prop;
        }
    }

    return g_hash_table_lookup(klass->properties, name);
}

 

마지막으로, 모든 부모 클래스를 (재귀적으로) 순회한 후에, g_hash_table_lookup(obj->properties, name);을 호출해서 자기 자신의 필드에서 찾는다.

 

object_property_find 탐색 흐름

 

객체 속성의 설정 (set)

앞에서 객체 속성의 추가 인터페이스를 다뤘다. 여기서는 객체 속성의 설정 인터페이스를 다룬다.

객체 속성의 설정은 object_property_set 함수를 통해 완성된다. 이 함수도 마찬가지로 qom/object.c에 있고, 코드는 다음과 같다.

bool object_property_set(Object *obj, const char *name, Visitor *v,
                         Error **errp)
{
    ERRP_GUARD();
    ObjectProperty *prop = object_property_find_err(obj, name, errp);

    if (prop == NULL) {
        return false;
    }

    if (!prop->set) {
        error_setg(errp, QERR_PERMISSION_DENIED);
        return false;
    }
    prop->set(obj, v, name, prop->opaque, errp);
    return !*errp;
}

 

사실 object_property_set 함수는 그냥 단순하게 ObjectProperty의 set 함수를 호출한 거다.

각 속성 타입마다 자기만의 set 함수가 있다. bool, str, enum 등이다. 

static void property_set_str(Object *obj, Visitor *v, const char *name,
                             void *opaque, Error **errp)
{
    StringProperty *prop = opaque;
    char *value;

    if (!visit_type_str(v, name, &value, errp)) {
        return;
    }

    prop->set(obj, value, errp);
    g_free(value);
}

 

static void property_set_enum(Object *obj, Visitor *v, const char *name,
                              void *opaque, Error **errp)
{
    EnumProperty *prop = opaque;
    int value;

    if (!visit_type_enum(v, name, &value, prop->lookup, errp)) {
        return;
    }
    prop->set(obj, value, errp);
}

 

static void property_set_bool(Object *obj, Visitor *v, const char *name,
                              void *opaque, Error **errp)
{
    BoolProperty *prop = opaque;
    bool value;

    if (!visit_type_bool(v, name, &value, errp)) {
        return;
    }

    prop->set(obj, value, errp);
}

 

속성 set 호출 체인 (bool 예시)

 

bool을 예로 들어서 설명한다. 보다시피, property_set_bool 함수는 구체적 속성(BoolProperty)의 set 함수를 호출했다. 이건 이 속성을 생성할 때 지정된 거다.

이전에 다뤘듯이, 객체 속성의 추가는 object_property_add 인터페이스를 통해 완성된다. bool 타입 객체 속성의 추가는 object_property_add_bool 함수가 이 인터페이스를 호출한다.

object_property_add_bool 함수는 qom/object.c에 있고, 코드는 다음과 같다

ObjectProperty *
object_property_add_bool(Object *obj, const char *name,
                         bool (*get)(Object *, Error **),
                         void (*set)(Object *, bool, Error **))
{
    BoolProperty *prop = g_malloc0(sizeof(*prop));

    prop->get = get;
    prop->set = set;

    return object_property_add(obj, name, "bool",
                               get ? property_get_bool : NULL,
                               set ? property_set_bool : NULL,
                               property_release_data,
                               prop);
}

이 함수에서 볼 수 있듯이, BoolProperty 구조체의 set 멤버 함수가 object_property_add_bool 함수를 호출할 때 전달되는 인자의 형식 매개변수인 void (*set)(Object *, bool, Error **)에 대응하는 실제 인자가 된다.

 

object_property_add_bool 함수에서 위의 property_set_bool 함수를 사용했고, property_set_bool 함수에서 최종적으로 이 형식 매개변수 void (*set)(Object *, bool, Error **)에 대응하는 실제 인자를 호출한다.

즉 진짜 객체 속성을 설정하는 함수다.

 

machine을 예로 들면, hw/core/machine.c의 machine_initfn 함수 코드는 다음과 같다.

static void machine_initfn(Object *obj)
{
    MachineState *ms = MACHINE(obj);
    MachineClass *mc = MACHINE_GET_CLASS(obj);

    container_get(obj, "/peripheral");
    container_get(obj, "/peripheral-anon");

    ms->dump_guest_core = true;
    ms->mem_merge = true;
    ms->enable_graphics = true;
    ms->kernel_cmdline = g_strdup("");
    ms->ram_size = mc->default_ram_size;
    ms->maxram_size = mc->default_ram_size;

    if (mc->nvdimm_supported) {
        Object *obj = OBJECT(ms);

        ms->nvdimms_state = g_new0(NVDIMMState, 1);
        object_property_add_bool(obj, "nvdimm",
                                 machine_get_nvdimm, machine_set_nvdimm);
        object_property_set_description(obj, "nvdimm",
                                        "Set on/off to enable/disable "
                                        "NVDIMM instantiation");

        object_property_add_str(obj, "nvdimm-persistence",
                                machine_get_nvdimm_persistence,
                                machine_set_nvdimm_persistence);
        object_property_set_description(obj, "nvdimm-persistence",
                                        "Set NVDIMM persistence"
                                        "Valid values are cpu, mem-ctrl");
    }

    if (mc->cpu_index_to_instance_props && mc->get_default_cpu_node_id) {
        ms->numa_state = g_new0(NumaState, 1);
        object_property_add_bool(obj, "hmat",
                                 machine_get_hmat, machine_set_hmat);
        object_property_set_description(obj, "hmat",
                                        "Set on/off to enable/disable "
                                        "ACPI Heterogeneous Memory Attribute "
                                        "Table (HMAT)");
    }

    /* default to mc->default_cpus */
    ms->smp.cpus = mc->default_cpus;
    ms->smp.max_cpus = mc->default_cpus;
    ms->smp.sockets = 1;
    ms->smp.dies = 1;
    ms->smp.clusters = 1;
    ms->smp.cores = 1;
    ms->smp.threads = 1;

    machine_copy_boot_config(ms, &(BootConfiguration){ 0 });
}

 

이 함수 중 아래 코드에 주목하자

object_property_add_bool(obj, "hmat",
                         machine_get_hmat, machine_set_hmat);

코드에서 machine_set_hmat 함수가 바로 object_property_add_bool 함수의 형식 매개변수 void (*set)(Object *, bool, Error **)에 대응하는 실제 인자다. 즉 machine 객체 속성을 설정할 때 실제로 호출되는 함수라는 거다.

 

객체 속성의 획득 (get)

앞에서 객체 속성의 설정 인터페이스를 다뤘다. 여기서는 객체 속성의 획득 인터페이스를 다룬다. 실제로 메커니즘은 매우 유사하다.

객체 속성의 획득은 object_property_get 함수를 통해 완성된다. 이 함수도 마찬가지로 qom/object.c에 있고, 코드는 다음과 같다

bool object_property_get(Object *obj, const char *name, Visitor *v,
                         Error **errp)
{
    Error *err = NULL;
    ObjectProperty *prop = object_property_find_err(obj, name, errp);

    if (prop == NULL) {
        return false;
    }

    if (!prop->get) {
        error_setg(errp, QERR_PERMISSION_DENIED);
        return false;
    }
    prop->get(obj, v, name, prop->opaque, &err);
    error_propagate(errp, err);
    return !err;
}

사실 object_property_get 함수도 그냥 단순하게 ObjectProperty의 get 함수를 호출한 거다.

각 속성 타입마다 자기만의 get 함수가 있다. bool, str, enum 등이다. 

static void property_get_str(Object *obj, Visitor *v, const char *name,
                             void *opaque, Error **errp)
{
    StringProperty *prop = opaque;
    char *value;
    Error *err = NULL;

    value = prop->get(obj, &err);
    if (err) {
        error_propagate(errp, err);
        return;
    }

    visit_type_str(v, name, &value, errp);
    g_free(value);
}
static void property_get_enum(Object *obj, Visitor *v, const char *name,
                              void *opaque, Error **errp)
{
    EnumProperty *prop = opaque;
    int value;
    Error *err = NULL;

    value = prop->get(obj, &err);
    if (err) {
        error_propagate(errp, err);
        return;
    }

    visit_type_enum(v, name, &value, prop->lookup, errp);
}
static void property_get_bool(Object *obj, Visitor *v, const char *name,
                              void *opaque, Error **errp)
{
    BoolProperty *prop = opaque;
    bool value;
    Error *err = NULL;

    value = prop->get(obj, &err);
    if (err) {
        error_propagate(errp, err);
        return;
    }

    visit_type_bool(v, name, &value, errp);
}

여전히 bool을 예로 들어 설명한다. 보다시피, property_get_bool 함수는 구체적 속성(BoolProperty)의 get 함수를 호출했다.

이건 이 속성을 생성할 때 지정된 거다. 이전에 이미 다뤘듯이, 객체 속성의 추가는 object_property_add 인터페이스를 통해 완성된다. bool 타입 객체 속성의 추가는 object_property_add_bool 함수가 이 인터페이스를 호출한다.

 

함수에서 볼 수 있듯이, BoolProperty 구조체의 get 멤버 함수가 object_property_add_bool 함수를 호출할 때 전달되는 인자의 형식 매개변수인 bool (*get)(Object *, Error **)에 대응하는 실제 인자가 된다. object_property_add_bool 함수에서 위의 property_get_bool 함수를 사용했고, property_get_bool 함수에서 최종적으로 이 형식 매개변수 bool (*get)(Object *, Error **)에 대응하는 실제 인자를 호출한다. 즉 진짜 객체 속성을 획득하는 함수라는 거다.

 

여전히 machine을 예로 들면, hw/core/machine.c의 machine_initfn 함수에서 아래 코드에 주목하자

object_property_add_bool(obj, "hmat",
                         machine_get_hmat, machine_set_hmat);

코드에서 machine_get_hmat 함수가 바로 object_property_add_bool 함수의 형식 매개변수 bool (*get)(Object *, Error **)에 대응하는 실제 인자다. 즉 machine 객체 속성을 획득할 때 실제로 호출되는 함수라는 거다.

여기까지가 객체의 속성 추가, 설정, 획득에 대한 설명이다. 다음 회에서는 클래스의 속성 추가, 설정, 획득을 다룬다.

 

클래스 속성의 추가

이전에 속성의 추가는 클래스 속성의 추가와 객체 속성의 추가로 나뉜다고 했다. 그 후로 쭉 객체 속성의 추가 쪽으로 내려왔는데, 이제 돌아와서 다른 한 갈래 — 클래스 속성의 추가를 다룬다.

 

객체 속성의 경우, 속성 추가는 qom/object.c의 object_property_add 인터페이스를 통해 완성된다. 그에 대응해서, 클래스의 속성 추가 함수는 object_class_property_add이고, 역시 qom/object.c에 있다 (실제로 object_property_add 함수 바로 아래에 있다). 코드는 다음과 같다.

ObjectProperty *
object_class_property_add(ObjectClass *klass,
                          const char *name,
                          const char *type,
                          ObjectPropertyAccessor *get,
                          ObjectPropertyAccessor *set,
                          ObjectPropertyRelease *release,
                          void *opaque)
{
    ObjectProperty *prop;

    assert(!object_class_property_find(klass, name));

    prop = g_malloc0(sizeof(*prop));

    prop->name = g_strdup(name);
    prop->type = g_strdup(type);

    prop->get = get;
    prop->set = set;
    prop->release = release;
    prop->opaque = opaque;

    g_hash_table_insert(klass->properties, prop->name, prop);

    return prop;
}

이 함수는 object_property_add 함수처럼 한 층 더 object_property_try_add 함수를 호출하지 않고, 이 함수 안에서 직접 object_property_try_add 함수와 유사한 기능을 구현했다 (코드가 매우 유사하다).

object_property_try_add 함수 코드와 비교해보자

ObjectProperty *
object_property_try_add(Object *obj, const char *name, const char *type,
                        ObjectPropertyAccessor *get,
                        ObjectPropertyAccessor *set,
                        ObjectPropertyRelease *release,
                        void *opaque, Error **errp)
{
    ObjectProperty *prop;
    size_t name_len = strlen(name);

    ......

    if (object_property_find(obj, name) != NULL) {
        error_setg(errp, "attempt to add duplicate property '%s' to object (type '%s')",
                   name, object_get_typename(obj));
        return NULL;
    }

    prop = g_malloc0(sizeof(*prop));

    prop->name = g_strdup(name);
    prop->type = g_strdup(type);

    prop->get = get;
    prop->set = set;
    prop->release = release;
    prop->opaque = opaque;

    g_hash_table_insert(obj->properties, prop->name, prop);

    return prop;
}

보면 알겠지만, 유사도가 매우 높다. 객체와 클래스의 속성 처리 흐름이 거의 같고, 그저 Object *obj를 ObjectClass *klass로 바꾼 것뿐이다.

 

객체 속성 추가 vs 클래스 속성 추가 비교

먼저 object_class_property_find 함수를 호출해서 삽입하려는 클래스 속성이 이미 존재하는지 확인한다. 이미 존재하면 에러를 리턴하고, 중복 속성이 추가되지 않도록 한다.

그 다음 하나의 ObjectProperty 구조체를 할당하고 함수 인자를 사용해서 초기화한다. 코드는 다음과 같다 (이 부분은 object_property_try_add와 완전히 같다)

prop = g_malloc0(sizeof(*prop));

    prop->name = g_strdup(name);
    prop->type = g_strdup(type);

    prop->get = get;
    prop->set = set;
    prop->release = release;
    prop->opaque = opaque;

 

마지막으로, 아래 코드를 호출해서 클래스의 properties 필드에 삽입하고, 이 prop을 리턴한다.

g_hash_table_insert(klass->properties, prop->name, prop);

 

사실 클래스의 속성 추가는 오히려 객체의 속성 추가보다 간단하다. 왜냐하면 객체의 속성 추가에서도 여기의 object_class_property_find 함수를 호출하기 때문이다.

이 함수의 역할은 이전에도 다뤘듯이, 자기가 속한 클래스와 모든 부모 클래스에 이 속성이 존재하지 않는지 확인하는 거다.

 

클래스 속성의 설정 및 획득

앞에서 클래스 속성의 추가 인터페이스를 다뤘다. 이 글에서는 클래스 속성의 설정 및 획득 인터페이스를 다룬다.

각 타입의 클래스 속성마다 자기만의 add 함수가 있다. bool, str, enum 등이다. 

ObjectProperty *
object_class_property_add_bool(ObjectClass *klass, const char *name,
                               bool (*get)(Object *, Error **),
                               void (*set)(Object *, bool, Error **))
{
    BoolProperty *prop = g_malloc0(sizeof(*prop));

    prop->get = get;
    prop->set = set;

    return object_class_property_add(klass, name, "bool",
                                     get ? property_get_bool : NULL,
                                     set ? property_set_bool : NULL,
                                     NULL,
                                     prop);
}

 

ObjectProperty *
object_class_property_add_enum(ObjectClass *klass, const char *name,
                               const char *typename,
                               const QEnumLookup *lookup,
                               int (*get)(Object *, Error **),
                               void (*set)(Object *, int, Error **))
{
    EnumProperty *prop = g_malloc(sizeof(*prop));

    prop->lookup = lookup;
    prop->get = get;
    prop->set = set;

    return object_class_property_add(klass, name, typename,
                                     get ? property_get_enum : NULL,
                                     set ? property_set_enum : NULL,
                                     NULL,
                                     prop);
}

 

ObjectProperty *
object_class_property_add_str(ObjectClass *klass, const char *name,
                              char *(*get)(Object *, Error **),
                              void (*set)(Object *, const char *,
                                          Error **))
{
    StringProperty *prop = g_malloc0(sizeof(*prop));

    prop->get = get;
    prop->set = set;

    return object_class_property_add(klass, name, "string",
                                     get ? property_get_str : NULL,
                                     set ? property_set_str : NULL,
                                     NULL,
                                     prop);
}

 

클래스 속성 add 함수 패턴 (str 예시)

이번에는 str을 예로 들어 설명한다. 이전에 이미 다뤘듯이, 클래스 속성의 추가는 object_class_property_add 인터페이스를 통해 완성된다. str 타입 클래스 속성의 추가는 object_class_property_add_str 함수가 이 인터페이스를 호출한다.

 

앞서 설명한 object_class_property_add 함수에 볼 수 있듯이, StringProperty 구조체의 set과 get 멤버 함수가 object_class_property_add_str 함수를 호출할 때 전달되는 인자의 형식 매개변수에 대응하는 실제 인자가 된다.

 

형식 매개변수는 각각 void (*set)(Object *, const char *, Error **)와 char *(*get)(Object *, Error **)이다. object_class_property_add_str 함수에서 위의 property_set_str과 property_get_str 함수를 사용했고, property_set_str 함수에서 최종적으로 형식 매개변수 void (*set)(Object *, const char *, Error **)에 대응하는 실제 인자, 즉 진짜 클래스 속성의 설정 함수를 호출한다.

property_get_str 함수에서 최종적으로 형식 매개변수 char *(*get)(Object *, Error **)에 대응하는 실제 인자, 즉 진짜 클래스 속성의 획득 함수를 호출한다.

 

machine을 예로 들면, hw/core/machine.c의 machine_class_init 함수 코드는 다음과 같다.

static void machine_class_init(ObjectClass *oc, void *data)
{
    MachineClass *mc = MACHINE_CLASS(oc);

    /* Default 128 MB as guest ram size */
    mc->default_ram_size = 128 * MiB;
    mc->rom_file_has_mr = true;

    /* numa node memory size aligned on 8MB by default.
     * On Linux, each node's border has to be 8MB aligned
     */
    mc->numa_mem_align_shift = 23;

    object_class_property_add_str(oc, "kernel",
        machine_get_kernel, machine_set_kernel);
    object_class_property_set_description(oc, "kernel",
        "Linux kernel image file");

    object_class_property_add_str(oc, "initrd",
        machine_get_initrd, machine_set_initrd);
    object_class_property_set_description(oc, "initrd",
        "Linux initial ramdisk file");

    object_class_property_add_str(oc, "append",
        machine_get_append, machine_set_append);
    object_class_property_set_description(oc, "append",
        "Linux kernel command line");

    object_class_property_add_str(oc, "dtb",
        machine_get_dtb, machine_set_dtb);
    object_class_property_set_description(oc, "dtb",
        "Linux kernel device tree file");

    object_class_property_add_str(oc, "dumpdtb",
        machine_get_dumpdtb, machine_set_dumpdtb);
    object_class_property_set_description(oc, "dumpdtb",
        "Dump current dtb to a file and quit");

    object_class_property_add(oc, "boot", "BootConfiguration",
        machine_get_boot, machine_set_boot,
        NULL, NULL);
    object_class_property_set_description(oc, "boot",
        "Boot configuration");

    object_class_property_add(oc, "smp", "SMPConfiguration",
        machine_get_smp, machine_set_smp,
        NULL, NULL);
    object_class_property_set_description(oc, "smp",
        "CPU topology");

    object_class_property_add(oc, "phandle-start", "int",
        machine_get_phandle_start, machine_set_phandle_start,
        NULL, NULL);
    object_class_property_set_description(oc, "phandle-start",
        "The first phandle ID we may generate dynamically");

    object_class_property_add_str(oc, "dt-compatible",
        machine_get_dt_compatible, machine_set_dt_compatible);
    object_class_property_set_description(oc, "dt-compatible",
        "Overrides the \"compatible\" property of the dt root node");

    object_class_property_add_bool(oc, "dump-guest-core",
        machine_get_dump_guest_core, machine_set_dump_guest_core);
    object_class_property_set_description(oc, "dump-guest-core",
        "Include guest memory in a core dump");

    object_class_property_add_bool(oc, "mem-merge",
        machine_get_mem_merge, machine_set_mem_merge);
    object_class_property_set_description(oc, "mem-merge",
        "Enable/disable memory merge support");

    object_class_property_add_bool(oc, "usb",
        machine_get_usb, machine_set_usb);
    object_class_property_set_description(oc, "usb",
        "Set on/off to enable/disable usb");

    object_class_property_add_bool(oc, "graphics",
        machine_get_graphics, machine_set_graphics);
    object_class_property_set_description(oc, "graphics",
        "Set on/off to enable/disable graphics emulation");

    object_class_property_add_str(oc, "firmware",
        machine_get_firmware, machine_set_firmware);
    object_class_property_set_description(oc, "firmware",
        "Firmware image");

    object_class_property_add_bool(oc, "suppress-vmdesc",
        machine_get_suppress_vmdesc, machine_set_suppress_vmdesc);
    object_class_property_set_description(oc, "suppress-vmdesc",
        "Set on to disable self-describing migration");

    object_class_property_add_link(oc, "confidential-guest-support",
        TYPE_CONFIDENTIAL_GUEST_SUPPORT,
        offsetof(MachineState, cgs),
        machine_check_confidential_guest_support,
        OBJ_PROP_LINK_STRONG);
    object_class_property_set_description(oc, "confidential-guest-support",
        "Set confidential guest scheme to support");

    /* For compatibility */
    object_class_property_add_str(oc, "memory-encryption",
        machine_get_memory_encryption, machine_set_memory_encryption);
    object_class_property_set_description(oc, "memory-encryption",
        "Set memory encryption object to use");

    object_class_property_add_link(oc, "memory-backend", TYPE_MEMORY_BACKEND,
        offsetof(MachineState, memdev), object_property_allow_set_link,
        OBJ_PROP_LINK_STRONG);
    object_class_property_set_description(oc, "memory-backend",
        "Set RAM backend"
        "Valid value is ID of hostmem based backend");

    object_class_property_add(oc, "memory", "MemorySizeConfiguration",
        machine_get_mem, machine_set_mem,
        NULL, NULL);
    object_class_property_set_description(oc, "memory",
        "Memory size configuration");
}

이 함수에서 많은 곳에서 object_class_property_add_str 함수를 호출했다. 그 중 하나를 예로 들면

object_class_property_add_str(oc, "kernel",
        machine_get_kernel, machine_set_kernel);

코드에서 machine_set_kernel과 machine_get_kernel 함수가 바로 object_class_property_add_str 함수의 형식 매개변수에 대응하는 실제 인자다.

즉 machine 클래스의 속성을 설정하고 획득할 때 실제로 호출되는 함수라는 의미이다.

 

QEMU 부팅부터 타입 초기화까지 전체 흐름

 

 

핵심 데이터 구조 관계 총정리

 

 

전체 요약 다이어그램

'System Programming > Hypervisor' 카테고리의 다른 글

QEMU 코드 분석 #6 VirtIO 기초 & 데이터 구조  (0) 2025.02.14
QEMU 코드 분석 #5 PCI 디바이스 에뮬레이션 & 인터럽트  (0) 2024.11.18
QEMU 코드 분석 #4 Machine & i440fx  (0) 2024.09.19
QEMU 코드 분석 #3 메모리 가상화 & 메모리 초기화  (0) 2024.07.08
QEMU 코드 분석 #2 KVM CPU가상화  (0) 2024.07.08
    'System Programming/Hypervisor' 카테고리의 다른 글
    • QEMU 코드 분석 #5 PCI 디바이스 에뮬레이션 & 인터럽트
    • QEMU 코드 분석 #4 Machine & i440fx
    • QEMU 코드 분석 #3 메모리 가상화 & 메모리 초기화
    • QEMU 코드 분석 #2 KVM CPU가상화
    1000sj
    1000sj

    티스토리툴바