-
* 개인 공부용도로 정리한 것이라 부정확한 정보가 있을 수도 있으니 참고용으로만 사용하세요.
IRQFD
irqfd는 QEMU에서 정의한 eventfd와 GSI(Global System Interrupt) 값을 이용해서 Guest에 바로 interrupt를 바로 쏘아 줄 때(irqfd_inject) 사용하는 kvm interrupt 라이브러리다. 간단한 event를 만들 때 사용하는 eventfd 메커니즘을 응용한 대표적인 예다.
좀더 디테일한 동작 내용을 이해하기 위해 초기화 코드를 순서대로 분석해보자.
1. QEMU 장치 정보 세팅 후 kvm으로 ioctl 전달
QEMU에서는 장치가 사용하려는 GSI(Global system interrupt)와 상태에 대한 정보를 저장하는 flag 값 그리고 qemu/VM 간 데이터를 주고받는 통로인 virtqueue의 file descriptor 값을 kvm에게 ioctl로 전달한다.
// qemu/accel/kvm/kvm-all.c static int kvm_irqchip_assign_irqfd(KVMState *s, int fd, int rfd, int virq, bool assign) { struct kvm_irqfd irqfd = { .fd = fd, .gsi = virq, .flags = assign ? 0 : KVM_IRQFD_FLAG_DEASSIGN, return kvm_vm_ioctl(s, KVM_IRQFD, &irqfd); }
2. irqfd 자료구조 생성 및 동작 함수 세팅
QEMU로부터 전달받은 정보들을 토대로 kvm은 현재 VM 이 사용할 irqfd 자료구조를 할당하고 값을 세팅한다. args로 참조하는 값은 QEMU에서 받아온 값이고 eventfd는 QEMU에서 전달받은 VirtQueue의 file descriptor 값을 이용해 추출한 주소다. VirtQueue가 eventfd 기반으로 만들어진 자료구조 인 것 같다. irqfd->inject,shutdown은 irqfd가 runtime에 수행할 함수들이다. irqfd_inject는 인터럽트를 guest에 주입하는 함수고 irqfd_shutdown은 irqfd 자료구조를 제거할 때 사용된다.
// linux/virt/kvm/eventfd.c static int kvm_irqfd_assign(struct kvm *kvm, struct kvm_irqfd *args) { irqfd->kvm = kvm; irqfd->gsi = args->gsi; INIT_LIST_HEAD(&irqfd->list); INIT_WORK(&irqfd->inject, irqfd_inject); INIT_WORK(&irqfd->shutdown, irqfd_shutdown); seqcount_init(&irqfd->irq_entry_sc); ... irqfd->eventfd = eventfd;
3. poll table, waitqueue 초기화
irqfd의 poll table의 콜백 함수와 waitqueue의 콜백 함수를 설정한다. 설정 목적은 eventfd 메커니즘으로 전달받은 이벤트를 irqfd로 쏘아주기 위함이다. eventfd부터 최종적으로 irqfd 함수가 불리기 까지의 동작은 마지막에 설명하도록 하자. 여기서는 poll_table이라는 것을 사용했다는 것에 주목하자
// linux/virt/kvm/eventfd.c static int kvm_irqfd_assign(struct kvm *kvm, struct kvm_irqfd *args) { init_waitqueue_func_entry(&irqfd->wait, irqfd_wakeup); init_poll_funcptr(&irqfd->pt, irqfd_ptable_queue_proc);
4. kvm irq routing 정보 입력
Guest로부터 넘겨받은 GSI 값에 대한 irq routing entry 정보를 받고 업데이트한다. 이부분은 배경 지식이 부족해 정확하게 이해하지 못한 부분이기는 한데 코드만 보면 대충 짐작하기로는 kvm 자료구조에서 irq routing에 대한 정보가 있으며 이 값은 GSI값에 따라 처리할 수 있는 entry 정보가 별도로 존재하는 것 같다. 추출한 entry 값을 irqfd 자료구조에 설정한다.
// linux/virt/kvm/eventfd.c static int kvm_irqfd_assign(struct kvm *kvm, struct kvm_irqfd *args) { irqfd_update(kvm, irqfd); static void irqfd_update(struct kvm *kvm, struct kvm_kernel_irqfd *irqfd) { // HK: Get the entries of irq_rt->map[gsi] // Set mapping between irqfd(kernel) to user space n_entries = kvm_irq_map_gsi(kvm, entries, irqfd->gsi); // linux/virt/kvm/irqchip.c int kvm_irq_map_gsi(struct kvm *kvm, struct kvm_kernel_irq_routing_entry *entries, int gsi) { irq_rt = srcu_dereference_check(kvm->irq_routing, &kvm->irq_srcu, lockdep_is_held(&kvm->irq_lock)); // HK: Define kvm_irq_routing_entry // Each entry properties depend on gsi value... if (irq_rt && gsi < irq_rt->nr_rt_entries) { hlist_for_each_entry(e, &irq_rt->map[gsi], link) { entries[n] = *e; ++n;
5. irqfd 리스트 추가 및 pending된 인터럽트 처리
지금까지 세팅한 irqfd 자료구조를 kvm irqfds 리스트에 추가한다. 장치가 여러개 있으니 VM별로 여러개의 irqfd를 가질 수 있다. 그리고 지금까지 초기화 하는 도중에 인터럽트가 발생했는지 확인하고 있으면 스케줄링으로 저리한다. 여기서 poll이라는 콜백함수로 이벤트가 있는지 확인하는데 poll 함수는 eventfd_poll이다. eventfd 값이 변경되면 새로운 event가 발생한 것으로 보고 interrupt를 날리려는 용도다.
// linux/virt/kvm/eventfd.c static int kvm_irqfd_assign(struct kvm *kvm, struct kvm_irqfd *args) { /* HK: Add new irqfd to kvm irqfd list */ list_add_tail(&irqfd->list, &kvm->irqfds.items); events = f.file->f_op->poll(f.file, &irqfd->pt); if (events & POLLIN) schedule_work(&irqfd->inject);
초기화 작업을 바탕으로 분석한 irqfd의 동작 과정은 다음과 같다
eventfd_poll => irqfd->pt (poll table) => (callback) irqfd_ptable_queue_proc
=> add_wait_queue(wqh, &irqfd->wait) => irqfd_wakeup => schedule_work(&irqfd->inject);
=> kvm_set_irq(kvm, KVM_USERSPACE_IRQ_SOURCE_ID, irqfd->gsi, 1,
eventfd_poll 함수에서는 irqfd가 갖고 있는 eventfd 값이 변경되길 기다리며 event 발생시 poll table에 물려있는 콜백함수(irqfd_ptable_queue_proc)함수가 불리고 이 함수에서는 waitqueue의 콜백 함수로 설정한 irqfd_wakeup 함수를 부른다. 이 함수에서는 interrupt를 날리는 함수인 irqfd_inject 함수가 불리고 이 함수에서 최종적으로 guest에게 interrupt를 쏴주게 된다.
휴 어렵네;kvm_set_irq 함수 내의 while문에서 콜백 함수로 set이 불리는데 이 함수는 아키텍처별로 설정되는 함수다. ARM의 경우에는 vgic의 vgic_irqfd_set_irq 함수가 불리며 x86 에서는 irqchip 타입에 따라서 콜백 함수가 다르다.
// virt/kvm/irqchip.c int kvm_set_irq(struct kvm *kvm, int irq_source_id, u32 irq, int level, bool line_status) { while (i--) { int r; // HK: kvm_set_msi architecture specific callback! // set -> vgic_irqfd_set_irq (vgic-irqfd.c) r = irq_set[i].set(&irq_set[i], kvm, irq_source_id, level, line_status);
사용 예시
Guest가 virtio 드라이버를 사용하는 경우, QEMU에서 에뮬레이션된 virtio backend 드라이버가 요청 받은 작업을 마치고 Guest에 VirtQueue를 이용해서 알림을 주게 된다. 이때 QEMU에서 VirtQueue에 결과 값을 입력하면 커널에서는 이전에 초기화한 VirtQueue의 eventfd 값이 변경된 것을 감지하고하고 초기화된 함수들이 연달아 불리게 된다. 최종적으로 interrupt_inject 함수가 불리게 되면서 Guest에게 interrupt가 전달된다.
'개발 > 가상화' 카테고리의 다른 글
QEMU를 이용해 커널 이미지 바꿔서 부팅해보기 (0) 2018.12.20 kvm ioeventfd (0) 2018.08.11 vhost (0) 2018.07.08 virtio (0) 2018.07.08 VFIO, Passthrough (0) 2018.06.30