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 코드 분석 #3 메모리 가상화 & 메모리 초기화

2024. 7. 8. 17:40

1) 메모리 가상화 개념 (GVA/GPA/HVA/HPA, 섀도우 페이지 테이블, EPT)
2) 핵심 자료구조 (AddressSpace, MemoryRegion, RAMBlock, FlatView, FlatRange)
3) 메모리 초기화 메인 라인 (pc_init1 메모리 분할 → cpu_exec_init_all → memory_map_init → memory_region_init → address_space_init)
4) 메모리 flattening (address_space_update_topology → generate_memory_topology → render_memory_region → flatviews_init / GHashTable)
5) listener (address_space_set_flatview)

 

1) 메모리 가상화 개념

메모리는 컴퓨터에서 빠질 수 없는 구성 요소 중 하나다.

따라서 메모리의 가상화도 모든 가상화 방안이 반드시 해결해야 하는 문제다.

 

CPU의 시각에서 보면, 물리 머신 위의 메모리는 0부터 시작하는 연속적으로 사용 가능한 물리 메모리다.

 

가상화에서는 모든 가상 머신이 0부터 시작하는, 연속적이고, 자기 자신에게 속하는 물리 주소가 필요하다. 이를 위해, VMM은 가상 머신을 위해서도 이런 공간을 모방해서 만들어내야 한다.

가장 먼저 비교적 쉽게 떠올릴 수 있는 것은 QEMU 프로세스의 가상 주소를 이런 공간으로 가상 머신에게 물리 메모리로 제공하는 것이다.

하지만 이런 방안에는 문제가 있다. 물리 머신에서, 보호 모드에서 CPU의 메모리 접근은 세그먼트와 페이징을 통해 구현된다.

 

즉 보호 모드에서 CPU가 접근할 때 사용하는 것은 가상 주소이지 물리 주소가 아니다.

가상 주소는 반드시 하드웨어 MMU를 통해 변환되어야 하며, 이를 물리 주소로 변환한 후에야 실제 물리 메모리에 접근할 수 있다.

 

MMU가 값을 가져와서 CPU에 반환하면, CPU가 그 값을 출력한다. 만약 두 개의 프로그램이라면, 같은 주소에 접근하더라도 물리 주소는 동일하지 않을 것이다.

 

하지만 가상화 하에서는, 가상 머신 내부에도 자기 자신의 보호 모드가 있다.

따라서, 가상 머신의 가상 CPU가 메모리 주소 지정을 할 때, 사용하는 것은 가상 머신 내부의 가상 주소다.

이것이 실제 물리 메모리에 접근하게 하려면,

먼저 이 가상 머신의 가상 주소를 가상 머신의 물리 주소로 변환해야 하고

그런 다음 가상 머신의 물리 주소를 QEMU의 가상 주소(즉 물리 머신의 가상 주소)로 변환해야 하며

마지막으로 QEMU의 가상 주소를 물리 머신의 물리 주소로 변환해야 데이터에 접근할 수 있다.

 

이 과정에서는 네 가지 종류의 메모리가 관여한다

  • 가상 머신의 가상 메모리 주소 (Guest OS Virtual Memory Address, GVA)
    • 가상 머신 안의 프로세스가 보는 메모리 공간이다.
  • 가상 머신의 물리 메모리 주소 (Guest OS Physical Memory Address, GPA)
    • 가상 머신 안의 운영체제가 보는 메모리이며, 이를 물리 메모리라고 본다.
  • 물리 머신의 가상 메모리 주소 (Host Virtual Memory Address, HVA)
    • 물리 머신 위의 QEMU 프로세스가 보는 메모리 공간이다.
  • 물리 머신의 물리 메모리 주소 (Host Physical Memory Address, HPA)
    • 물리 머신 위의 운영체제가 보는 메모리다.

즉, 위쪽 가상 머신 안의 프로세스가 물리 메모리에 접근할 때, GVA->GPA->HVA->HPA 과정을 거쳐야 한다.

우리가 방관자로서 옆에서 이 과정을 지켜보기만 해도 너무 번거롭게 느껴지는데, 실제 효과는 말할 것도 없다.

 

이 특별히 "돌아가는" 과정을 거치면 컴퓨터의 성능은 완전히 이미 끝난 것이다.

분명히, 이는 가상화 소프트웨어가 일종의 메커니즘을 제공해서 이런 변환을 완수해야 한다(그리고 단순히 위의 과정만으로는 안 된다). 이것이 바로 소위 MMU의 가상화다.

 

섀도우 페이지 테이블과 EPT

CPU가 EPT(Extended Page Table, 확장 페이지 테이블)를 지원하기 전에는, 가상 머신은 섀도우 페이지 테이블을 통해 가상 머신 가상 주소에서 호스트 물리 주소로의 변환을 구현했다.

 

이는 일종의 소프트웨어 구현 방안이다. 이 방안에서, 섀도우 페이지 테이블은 직접 GVA에서 HPA로의 변환을 구현하며, 중간의 GPA와 HVA를 우회한다.

 

이 과정에는 중개 계층이 없지만, 전혀 비용이 절감되지 않는다.

가상화 소프트웨어(KVM)는 가상 머신 안의 각 프로세스를 위해 페이지 테이블을 하나씩 저장하며, 가상 머신 안의 프로세스도 자기 자신의 페이지 테이블이 있다.

 

하지만 이 페이지 테이블은 읽을 수 있다.

따라서 앞의 페이지 테이블을 뒤 페이지 테이블의 섀도우 페이지 테이블이라고 부른다.

이렇게 하면 가상 머신의 페이지 테이블은 VM Exit(즉 비루트 모드에서 루트 모드로 전환)를 일으키게 된다. 그러면 KVM이 이 요청을 처리하고, 섀도우 페이지 테이블을 업데이트한다.

 

섀도우 페이지 테이블의 기본 원리는 이렇다

 

1) 비루트 모드에서 CPU가 CR3을 수정하는 동작이 있을 때(보통 다음 프로세스가 스케줄링될 때), 페이지 디렉터리 주소가 CR3에 로드된다는 것을 의미한다(만약 32비트 페이징 모드라면). 이때 VMM이 이 동작을 가로채고, 루트 모드로 복귀한다.

 

2) VMM 안에서는 hash 링크 테이블을 유지하며, 이는 CR3 안의 페이지 디렉터리 주소에 대응하는 섀도우 주소를 저장하는 용도다. VMM은 페이지 디렉터리 주소를 받은 후 해당 주소의 hash 값을 계산해서 key로 hash 테이블에 넣고, 그 다음 호스트의 물리 주소를 페이지 디렉터리 주소로 할당받아 CR3 레지스터에 넣는다. 동시에 호스트의 물리 주소도 hash 테이블에 저장되며, CR3에 원래 있어야 할 페이지 디렉터리 주소는 VMM이 보관해둔다.

 

3) 가상 머신 프로세스가 스케줄 아웃될 때, CR3에 보관되어 있어야 할 페이지 디렉터리 주소가 프로세스 제어 블록에 다시 들어가야 한다. 이때 VMM이 이 동작을 가로채고 이전에 보관해둔 페이지 디렉터리 주소를 제공한다.

VMM은 CR3 레지스터가 수정되는 과정에서, 본래 CR3에 들어가야 할 주소를 빼내고, 자기가 할당받은 주소로 바꾸며, 게스트가 CR3에서 이 주소를 가져오려고 할 때 다시 바꿔준다.

섀도우 페이지 테이블 방안은 순수 소프트웨어로 MMU를 구현한 것이다.

 

EPT (Extended Page Table, 확장 페이지 테이블)

섀도우 페이지 테이블이 순수 소프트웨어로 MMU를 구현하는 방안은 효율이 너무 낮기 때문에, VMX 아키텍처는 소위 확장 페이지 테이블, 즉 EPT 방안을 도입했다.

EPT 방안에서, CPU의 주소 지정 모드는 VM non-root operation 하에서 변화가 발생하며, 이는 두 개의 페이지 테이블을 사용한다. 그 기본 원리는 아래 그림과 같다.

만약 EPT를 활성화하면, CPU가 VM Entry를 할 때 EPT 기능을 사용한다.

 

가상 머신은 내부 자체 페이지 테이블에 대해 완전한 통제권을 가지고 있고, CPU는 먼저 가상 머신 내부의 가상 주소를 가상 머신 물리 주소로 변환하는데, 이 과정에서는 가상 머신 내부의 페이지 테이블을 조회한다.

 

그런 다음 CPU는 이 주소를 호스트의 물리 주소로 변환하는데, 이 과정에서는 호스트 안의 EPT 페이지 테이블을 조회한다.

CPU가 VM Exit를 발생시키면, EPT 페이지가 닫히고, 이때 CPU는 호스트 위에서 다시 전통적인 단일 페이지 방식으로 주소 지정을 한다.

가상 머신 안의 페이지 테이블은 물리 머신 운영체제 페이지 테이블과 마찬가지로, 페이지 테이블 안에 저장된 것은 가상 머신 물리 주소이며, 자체적으로 관리된다.

EPT 페이지 테이블은 호스트가 관리하며, 안에 가상 머신 물리 주소에서 호스트 물리 주소로의 변환을 기록한다.

 

EPT는 섀도우 페이지 테이블이 소프트웨어로 GVA->HPA 주소 변환을 유지하는 단점을 극복했으며, 하드웨어를 사용해서 GPA->HPA를 유지한다.

따라서 효율이 크게 향상됐다. 게다가, 섀도우 페이지 테이블은 비록 주소 변환 경로를 단축하긴 했지만, 가상 머신 프로세스가 CR3에 접근할 때마다 VMX의 모드 전환을 일으키고, 비용이 매우 컸다.

 

섀도우 페이지 테이블은 매번 로드되고 언로드될 때마다 모드 전환을 일으켰지만, EPT는 이런 비용을 줄였다. EPT는 페이지 폴트가 일어날 때만 VMX 모드 전환을 일으키며, 페이지 테이블이 한번 만들어진 후에는 EPT는 더 이상 모드 전환의 비용이 없고, 가상 머신 메모리의 접근은 계속 게스트 상태에 있다.

 

일반적으로, EPT가 사용하는 것은 IA-32e의 페이징 모드이며, 즉 48비트 물리 주소를 사용하고, 총 4단계 페이지 테이블로 나뉘며, 각 페이지 테이블은 9비트 물리 주소를 사용하고, 마지막 12비트는 한 페이지(4KB) 내의 오프셋을 표시한다.

EPT 페이징 기본 주소 하에서, 가상 머신이 메모리 접근을 하면 일련의 GPA에서 HPA로의 변환을 일으킨다.

 

위 그림은 EPT의 접근 과정을 보여준다.

EPT 페이지 테이블의 기본 주소는 VMCS(Virtual Machine Control Structure) 구조 안의 EPTP(extended-page-table pointer) 포인터에 저장된다.

 

한 번의 메모리 접근을 할 때, 가상 머신은 먼저 게스트의 가상 메모리 주소(GVA)를 게스트 물리 메모리 주소(GPA)로 변환하는데, 이 중간에 가상 머신의 페이지 테이블을 조회하게 된다.

만약 대응하는 페이지 항목이 존재하고 유효하면, 이 페이지 항목 정보를 읽고, 그 다음 다음 단계의 페이지 테이블을 가져와서, 가상 머신이 접근해야 할 GPA를 계산한다.

 

그런 다음 이 GPA는 다시 한 번 페이지 테이블 조회를 수행하여, 최종적으로 물리 머신 위의 데이터를 찾는다.

 

이상의 과정에서, 만약 가상 머신 내부에 페이지 폴트 예외가 발생하면, 가상 머신은 스스로 자신의 페이지 테이블을 수정한다.

만약 EPT 예외가 발생하면, EPT 예외 종료가 발생하고, 가상 머신은 KVM으로 종료되어, 대응하는 EPT 페이지 테이블을 구축한다.

 

2) 핵심 자료구조

QEMU 메모리 관리의 다섯 핵심 구조

  • AddressSpace(접근 가능한 전체 주소 공간)
  • MemoryRegion(메모리 영역의 트리 노드)
  • RAMBlock(실제 할당된 물리 메모리 조각)
  • FlatView / FlatRange(트리를 평면화)

1. AddressSpace

 AddressSpace 구조는 한 가상 머신 또는 가상 CPU가 접근할 수 있는 모든 물리 주소를 표시하는 데 사용된다.

 

주의할 점은, 여기서 말하는 "접근"과 "접근할 수 있다"는 같은 일이 아니라는 것이다.

 

한 프로세스의 주소 공간과 마찬가지로, 한 프로세스의 가상 주소 공간이 4GB(32비트)지만, 이것이 운영체제가 프로세스에게 이렇게 큰 공간을 할당해야 한다는 의미는 아니다.

 

마찬가지로, QEMU 안의 AddressSpace는 하나의 주소 공간을 표시하며, 전체 시스템은 하나의 전역 주소 공간을 가질 수 있고, CPU도 자기 자신의 주소 공간 시점을 가질 수 있으며, 장치도 자기 자신의 주소 공간 시점을 가질 수 있다.

 

AddressSpace는 이름에서 보듯이, 전체 주소 공간을 대상으로 하며, 최상위 메모리 관리 구조다. x86 아키텍처에 있어, 사실 두 가지 주소 지정 방식이 있는데, 하나는 memory, 하나는 IO다. 그에 대응하여, QEMU는 두 개의 AddressSpace 구조를 유지한다. address_space_memory와 address_space_io다.

 

(include/qemu/typedefs.h)

typedef struct AddressSpace AddressSpace;

 

(include/exec/memory.h)

/**
 * struct AddressSpace: describes a mapping of addresses to #MemoryRegion objects
 */
struct AddressSpace {
    /* private: */
    struct rcu_head rcu;
    char *name;
    MemoryRegion *root;

    /* Accessed via RCU.  */
    struct FlatView *current_map;

    int ioeventfd_nb;
    struct MemoryRegionIoeventfd *ioeventfds;
    QTAILQ_HEAD(, MemoryListener) listeners;
    QTAILQ_ENTRY(AddressSpace) address_spaces_link;
};

struct AddressSpace의 주석에 따르면, 이 구조는 MemoryRegion 객체로의 주소 매핑을 묘사한다.

그 중에서

  • MemoryRegion *root 멤버는 AddressSpace에 대응하는 하나의 루트 MemoryRegion을 가리킨다.
  • struct FlatView *current_map 멤버는 이 주소 공간이 flattened 모드에서의 뷰임을 표시한다.
  • QEMU의 다른 서브시스템은 주소 변경 이벤트를 등록할 수 있으며, 모든 등록된 정보는 모두 QTAILQ_HEAD(, MemoryListener) listeners 멤버로 연결되어 있다.
  • 모든 AddressSpace는 address_spaces_link 이 노드를 통해 연결되어 있고 (QTAILQ_ENTRY(AddressSpace) address_spaces_link), 링크 헤드는 address_spaces다.

AddressSpace의 정의는 사실 비교적 간단하다.

두 개의 QEMU 메모리 관리 프레임워크의 핵심 구조가 있는데, 바로 MemoryRegion과 FlatView다.

둘을 결합해서 사용되며, MemoryRegion은 기초(MemoryRegion)이고, 정방향 관리에 사용된다.

Flatview는 역방향 조회에 사용된다.

/**
 * struct AddressSpace: describes a mapping of addresses to #MemoryRegion objects
 */
struct AddressSpace {
    /* private: */
    struct rcu_head rcu;
    char *name;
    MemoryRegion *root;     // ← 강조 표시

    /* Accessed via RCU.  */
    struct FlatView *current_map;   // ← 강조 표시

    int ioeventfd_nb;
    struct MemoryRegionIoeventfd *ioeventfds;
    QTAILQ_HEAD(, MemoryListener) listeners;
    QTAILQ_ENTRY(AddressSpace) address_spaces_link;
};

하나의 주소 공간(AddressSpace)에는, 여러 메모리 영역(MemoryRegion)이 트리 구조를 구성한다.

그 중, root는 이 트리의 루트다. 그밖에, 또 하나의 MemoryListener 링크가 있으며, 메모리 영역에 변화가 발생할 때, 일부 동작을 해야 하는데, 사용자 모드와 커널 모드가 협력할 수 있게 하기 위함이며, 바로 이 MemoryListener들이 완성하는 것이다.

 

QEMU monitor에 "info mtree"를 입력하면, 모든 AddressSpace를 볼 수 있다.

$ qemu-system-x86_64 -vnc :1 -monitor stdio
QEMU 6.2.0 monitor - type 'help' for more information
(qemu) info mtree
address-space: memory
  0000000000000000-ffffffffffffffff (prio 0, i/o): system
    0000000000000000-00000000ffffffff (prio 0, ram): alias ram-below-4g @pc.ram 0000000000000000-00000000...
    0000000000000000-ffffffffffffffff (prio -1, i/o): pci
      00000000000a0000-00000000000bffff (prio 1, i/o): vga-lowmem
      00000000000c0000-00000000000dffff (prio 1, rom): pc.rom
      00000000000e0000-00000000000fffff (prio 1, rom): alias isa-bios @pc.bios 0000000000020000-000000000003...
      00000000fd000000-00000000fdffffff (prio 1, ram): vga.vram
      00000000feb80000-00000000feb9ffff (prio 1, i/o): e1000-mmio
      00000000febb0000-00000000febbffff (prio 1, i/o): vga.mmio
        00000000febb0000-00000000febb017f (prio 0, i/o): edid
      00000000febb0400-00000000febb041f (prio 0, i/o): vga ioports remapped
      00000000febb0500-00000000febb0515 (prio 0, i/o): bochs dispi interface
      00000000febb0600-00000000febb0607 (prio 0, i/o): qemu extended regs
      00000000fffc0000-00000000ffffffff (prio 0, rom): pc.bios
    00000000000a0000-00000000000bffff (prio 1, i/o): alias smram-region @pci 00000000000a0000-00000000000bf...
    00000000000c0000-00000000000c3fff (prio 1, ram): alias pam-rom @pc.ram 00000000000c0000-00000000000c3ff...
    00000000000c4000-00000000000c7fff (prio 1, ram): alias pam-rom @pc.ram 00000000000c4000-00000000000c7ff...
    00000000000c8000-00000000000cbfff (prio 1, ram): alias pam-rom @pc.ram 00000000000c8000-00000000000cbff...
    00000000000cb000-00000000000cdfff (prio 1000, ram): alias kvmapic-rom @pc.ram 00000000000cb000-0000000...
    00000000000cc000-00000000000cffff (prio 1, ram): alias pam-rom @pc.ram 00000000000cc000-00000000000cfff...
    00000000000d0000-00000000000d3fff (prio 1, ram): alias pam-rom @pc.ram 00000000000d0000-00000000000d3ff...
    00000000000d4000-00000000000d7fff (prio 1, ram): alias pam-rom @pc.ram 00000000000d4000-00000000000d7ff...
    00000000000d8000-00000000000dbfff (prio 1, ram): alias pam-rom @pc.ram 00000000000d8000-00000000000dbff...
    00000000000dc000-00000000000dffff (prio 1, ram): alias pam-rom @pc.ram 00000000000dc000-00000000000dfff...
    00000000000e0000-00000000000e3fff (prio 1, ram): alias pam-rom @pc.ram 00000000000e0000-00000000000e3ff...
    00000000000e4000-00000000000e7fff (prio 1, ram): alias pam-rom @pc.ram 00000000000e4000-00000000000e7ff...
    00000000000e8000-00000000000ebfff (prio 1, ram): alias pam-ram @pc.ram 00000000000e8000-00000000000ebff...
    00000000000ec000-00000000000efff f (prio 1, ram): alias pam-ram @pc.ram 00000000000ec000-00000000000efff...
    00000000000f0000-00000000000fffff (prio 1, ram): alias pam-ram @pc.ram 00000000000f0000-00000000000ffff...
    00000000fec00000-00000000fec00fff (prio 0, i/o): ioapic
    00000000fed00000-00000000fed003ff (prio 0, i/o): hpet
    00000000fee00000-00000000feefffff (prio 4096, i/o): apic-msi

address-space: I/O
  0000000000000000-000000000000ffff (prio 0, i/o): io
    0000000000000000-0000000000000007 (prio 0, i/o): dma-chan
    0000000000000008-000000000000000f (prio 0, i/o): dma-cont
    0000000000000020-0000000000000021 (prio 0, i/o): pic
    0000000000000040-0000000000000043 (prio 0, i/o): pit
    0000000000000060-0000000000000060 (prio 0, i/o): i8042-data
    0000000000000061-0000000000000061 (prio 0, i/o): pcspk
    0000000000000064-0000000000000064 (prio 0, i/o): i8042-cmd
    0000000000000070-0000000000000071 (prio 0, i/o): rtc
      0000000000000070-0000000000000070 (prio 0, i/o): rtc-index
    000000000000007e-000000000000007f (prio 0, i/o): kvmvapic
    0000000000000080-0000000000000080 (prio 0, i/o): ioport80
    0000000000000081-0000000000000083 (prio 0, i/o): dma-page
    0000000000000087-0000000000000087 (prio 0, i/o): dma-page
    0000000000000089-000000000000008b (prio 0, i/o): dma-page
    000000000000008f-000000000000008f (prio 0, i/o): dma-page
    0000000000000092-0000000000000092 (prio 0, i/o): port92
    00000000000000a0-00000000000000a1 (prio 0, i/o): pic
    00000000000000b2-00000000000000b3 (prio 0, i/o): apm-io
    00000000000000c0-00000000000000cf (prio 0, i/o): dma-chan
    00000000000000d0-00000000000000df (prio 0, i/o): dma-cont
    00000000000000f0-00000000000000f0 (prio 0, i/o): ioportF0
    0000000000000170-0000000000000177 (prio 0, i/o): ide
    00000000000001ce-00000000000001d1 (prio 0, i/o): vbe
    00000000000001f0-00000000000001f7 (prio 0, i/o): ide
    0000000000000376-0000000000000376 (prio 0, i/o): ide
    0000000000000378-000000000000037f (prio 0, i/o): parallel
    00000000000003b4-00000000000003b5 (prio 0, i/o): vga
    00000000000003ba-00000000000003ba (prio 0, i/o): vga
    00000000000003c0-00000000000003cf (prio 0, i/o): vga
    00000000000003d4-00000000000003d5 (prio 0, i/o): vga
    00000000000003da-00000000000003da (prio 0, i/o): vga
    00000000000003f1-00000000000003f5 (prio 0, i/o): fdc
    00000000000003f6-00000000000003f6 (prio 0, i/o): ide
    00000000000003f7-00000000000003f7 (prio 0, i/o): fdc
    00000000000003f8-00000000000003ff (prio 0, i/o): serial
    00000000000004d0-00000000000004d0 (prio 0, i/o): elcr
    00000000000004d1-00000000000004d1 (prio 0, i/o): elcr
    0000000000000510-0000000000000511 (prio 0, i/o): fwcfg
    0000000000000514-000000000000051b (prio 0, i/o): fwcfg.dma
    0000000000000600-000000000000063f (prio 0, i/o): piix4-pm
    0000000000000600-0000000000000603 (prio 0, i/o): acpi-evt
    0000000000000604-0000000000000605 (prio 0, i/o): acpi-cnt
    0000000000000608-000000000000060b (prio 0, i/o): acpi-tmr
    0000000000000700-0000000000000707 (prio 0, i/o): pm-smbus
    0000000000000cf8-0000000000000cfb (prio 0, i/o): pci-conf-idx
    0000000000000cf9-0000000000000cf9 (prio 1, i/o): piix3-reset-control
    0000000000000cfc-0000000000000cff (prio 0, i/o): pci-conf-data
    0000000000005658-0000000000005658 (prio 0, i/o): vmport
    0000000000000ae00-0000000000000ae17 (prio 0, i/o): acpi-pci-hotplug
    00000000af00-00000000af1f (prio 0, i/o): acpi-cpu-hotplug
    00000000afe0-00000000afe3 (prio 0, i/o): acpi-gpe0
    00000000c000-00000000c03f (prio 1, i/o): e1000-io
    00000000c040-00000000c04f (prio 0, i/o): piix-bmdma-container
      00000000c040-00000000c043 (prio 0, i/o): piix-bmdma
      00000000c044-00000000c047 (prio 0, i/o): bmdma
      00000000c048-00000000c04b (prio 0, i/o): piix-bmdma
      00000000c04c-00000000c04f (prio 0, i/o): bmdma

address-space: cpu-memory-0
  0000000000000000-ffffffffffffffff (prio 0, i/o): system
    0000000000000000-00000000ffffffff (prio 0, ram): alias ram-below-4g @pc.ram 0000000000000000-00000000ffffffff
    0000000000000000-ffffffffffffffff (prio -1, i/o): pci
      00000000000a0000-00000000000bffff (prio 1, i/o): vga-lowmem
      00000000000c0000-00000000000dffff (prio 1, rom): pc.rom
      00000000000e0000-00000000000fffff (prio 1, rom): alias isa-bios @pc.bios 0000000000020000-0000000000040000
      00000000fd000000-00000000fdffffff (prio 1, ram): vga.vram
      00000000feb80000-00000000feb9ffff (prio 1, i/o): e1000-mmio
      00000000febb0000-00000000febbffff (prio 1, i/o): vga.mmio
        00000000febb0000-00000000febb017f (prio 0, i/o): edid
        00000000febb0400-00000000febb041f (prio 0, i/o): vga ioports remapped
        00000000febb0500-00000000febb0515 (prio 0, i/o): bochs dispi interface
        00000000febb0600-00000000febb0607 (prio 0, i/o): qemu extended regs
      00000000fffc0000-00000000ffffffff (prio 1, rom): pc.bios
    00000000000a0000-00000000000bffff (prio 1, i/o): alias smram-region @pci 00000000000a0000-00000000000bffff
    00000000000c0000-00000000000c3fff (prio 1, ram): alias pam-rom @pc.ram 00000000000c0000-00000000000c3fff
    00000000000c4000-00000000000c7fff (prio 1, ram): alias pam-rom @pc.ram 00000000000c4000-00000000000c7fff
    00000000000c8000-00000000000cbfff (prio 1, ram): alias pam-rom @pc.ram 00000000000c8000-00000000000cbfff
    00000000000cb000-00000000000cdfff (prio 1000, ram): alias kvmapic-rom @pc.ram 00000000000cb000-00000000000cdfff
    00000000000cc000-00000000000cffff (prio 1, ram): alias pam-rom @pc.ram 00000000000cc000-00000000000cffff
    00000000000d0000-00000000000d3fff (prio 1, ram): alias pam-rom @pc.ram 00000000000d0000-00000000000d3fff
    00000000000d4000-00000000000d7fff (prio 1, ram): alias pam-rom @pc.ram 00000000000d4000-00000000000d7fff
    00000000000d8000-00000000000dbfff (prio 1, ram): alias pam-rom @pc.ram 00000000000d8000-00000000000dbfff
    00000000000dc000-00000000000dffff (prio 1, ram): alias pam-rom @pc.ram 00000000000dc000-00000000000dffff
    00000000000e0000-00000000000e3fff (prio 1, ram): alias pam-rom @pc.ram 00000000000e0000-00000000000e3fff
    00000000000e4000-00000000000e7fff (prio 1, ram): alias pam-rom @pc.ram 00000000000e4000-00000000000e7fff
    00000000000e8000-00000000000ebfff (prio 1, ram): alias pam-ram @pc.ram 00000000000e8000-00000000000ebfff
    00000000000ec000-00000000000efff f (prio 1, ram): alias pam-ram @pc.ram 00000000000ec000-00000000000efff f
    00000000000f0000-00000000000fffff (prio 1, ram): alias pam-ram @pc.ram 00000000000f0000-00000000000fffff
    00000000fec00000-00000000fec00fff (prio 0, i/o): ioapic
    00000000fed00000-00000000fed003ff (prio 0, i/o): hpet
    00000000fee00000-00000000feefffff (prio 4096, i/o): apic-msi

address-space: cpu-smm-0
  0000000000000000-ffffffffffffffff (prio 0, i/o): memory
    0000000000000000-00000000ffffffff (prio 1, i/o): alias smram @smram 0000000000000000-00000000ffffffff
    0000000000000000-ffffffffffffffff (prio 0, i/o): alias memory @system 0000000000000000-ffffffffffffffff

address-space: i440FX
  0000000000000000-ffffffffffffffff (prio 0, i/o): bus master container

address-space: PIIX3
  0000000000000000-ffffffffffffffff (prio 0, i/o): bus master container

address-space: VGA
  0000000000000000-ffffffffffffffff (prio 0, i/o): bus master container

address-space: e1000
  0000000000000000-ffffffffffffffff (prio 0, i/o): bus master container
    0000000000000000-ffffffffffffffff (prio 0, i/o): alias bus master @system 0000000000000000-ffffffffffffffff

address-space: piix3-ide
  0000000000000000-ffffffffffffffff (prio 0, i/o): bus master container

address-space: PIIX4_PM
  0000000000000000-ffffffffffffffff (prio 0, i/o): bus master container

memory-region: pc.ram
  0000000000000000-0000000007ffffff (prio 0, ram): pc.ram

memory-region: pc.bios
  00000000fffc0000-00000000ffffffff (prio 0, rom): pc.bios

memory-region: pci
  0000000000000000-ffffffffffffffff (prio -1, i/o): pci
    00000000000a0000-00000000000bffff (prio 1, i/o): vga-lowmem
    00000000000c0000-00000000000dffff (prio 1, rom): pc.rom
    00000000000e0000-00000000000fffff (prio 1, rom): alias isa-bios @pc.bios 0000000000020000-000000000003f...
    00000000fd000000-00000000fdffffff (prio 1, ram): vga.vram
    00000000feb80000-00000000feb9ffff (prio 1, i/o): e1000-mmio
    00000000febb0000-00000000febbffff (prio 1, i/o): vga.mmio
      00000000febb0000-00000000febb017f (prio 0, i/o): edid
      00000000febb0400-00000000febb041f (prio 0, i/o): vga ioports remapped
      00000000febb0500-00000000febb0515 (prio 0, i/o): bochs dispi interface
      00000000febb0600-00000000febb0607 (prio 0, i/o): qemu extended regs
    00000000fffc0000-00000000ffffffff (prio 0, rom): pc.bios

memory-region: smram
  0000000000000000-00000000ffffffff (prio 0, i/o): smram
    00000000000a0000-00000000000bffff (prio 0, ram): alias smram-low @pc.ram 00000000000a0000-00000000000bf

memory-region: system
  0000000000000000-ffffffffffffffff (prio 0, i/o): system
    0000000000000000-00000000ffffffff (prio 0, ram): alias ram-below-4g @pc.ram 0000000000000000-00000000fff
    0000000000000000-ffffffffffffffff (prio -1, i/o): pci
      00000000000a0000-00000000000bffff (prio 1, i/o): vga-lowmem
      00000000000c0000-00000000000dffff (prio 1, rom): pc.rom
      00000000000e0000-00000000000fffff (prio 1, rom): alias isa-bios @pc.bios 0000000000020000-0000000000040...
      00000000fd000000-00000000fdffffff (prio 1, ram): vga.vram
      00000000feb80000-00000000feb9ffff (prio 1, i/o): e1000-mmio
      00000000febb0000-00000000febbffff (prio 1, i/o): vga.mmio
        00000000febb0000-00000000febb017f (prio 0, i/o): edid
        00000000febb0400-00000000febb041f (prio 0, i/o): vga ioports remapped
        00000000febb0500-00000000febb0515 (prio 0, i/o): bochs dispi interface
        00000000febb0600-00000000febb0607 (prio 0, i/o): qemu extended regs
      00000000fffc0000-00000000ffffffff (prio 0, rom): pc.bios
    00000000000a0000-00000000000bffff (prio 1, i/o): alias smram-region @pci 00000000000a0000-00000000000bf
    00000000000c0000-00000000000c3fff (prio 1, ram): alias pam-rom @pc.ram 00000000000c0000-00000000000c3ff
    00000000000c4000-00000000000c7fff (prio 1, ram): alias pam-rom @pc.ram 00000000000c4000-00000000000c7ff
    00000000000c8000-00000000000cbfff (prio 1, ram): alias pam-rom @pc.ram 00000000000c8000-00000000000cbff
    00000000000cb000-00000000000cdfff (prio 1000, ram): alias kvmapic-rom @pc.ram 00000000000cb000-0000000
    00000000000cc000-00000000000cffff (prio 1, ram): alias pam-rom @pc.ram 00000000000cc000-00000000000cfff
    00000000000d0000-00000000000d3fff (prio 1, ram): alias pam-rom @pc.ram 00000000000d0000-00000000000d3ff
    00000000000d4000-00000000000d7fff (prio 1, ram): alias pam-rom @pc.ram 00000000000d4000-00000000000d7ff
    00000000000d8000-00000000000dbfff (prio 1, ram): alias pam-rom @pc.ram 00000000000d8000-00000000000dbff
    00000000000dc000-00000000000dffff (prio 1, ram): alias pam-rom @pc.ram 00000000000dc000-00000000000dfff
    00000000000e0000-00000000000e3fff (prio 1, ram): alias pam-rom @pc.ram 00000000000e0000-00000000000e3ff
    00000000000e4000-00000000000e7fff (prio 1, ram): alias pam-ram @pc.ram 00000000000e4000-00000000000e7ff
    00000000000e8000-00000000000ebfff (prio 1, ram): alias pam-ram @pc.ram 00000000000e8000-00000000000ebff
    00000000000ec000-00000000000efff f (prio 1, ram): alias pam-ram @pc.ram 00000000000ec000-00000000000efff
    00000000000f0000-00000000000fffff (prio 1, ram): alias pam-ram @pc.ram 00000000000f0000-00000000000ffff
    00000000fec00000-00000000fec00fff (prio 0, i/o): ioapic
    00000000fed00000-00000000fed003ff (prio 0, i/o): hpet
    00000000fee00000-00000000feefffff (prio 4096, i/o): apic-msi
  • address-space: memory

첫 번째는 시스템 전역의 AddressSpace이고, 가상 머신이 접근할 수 있는 모든 주소를 표시한다.

  • address-space: I/O

I/O는 x86 시스템 하에서 I/O 포트의 주소 공간을 표시한다.

  • address-space: cpu-memory-0

cpu-memory-x는 CPU 시각 하에서의 주소 공간을 표시한다.

  • address-space: i440FX와 address-space: PIIX3

i440FX와 PIIX3는 장치 시각 하에서의 주소 공간이며, 비록 대부분의 경우에는 거의 모두 같지만, 논리적인 의미는 같지 않다.

address-space: i440FX
  0000000000000000-ffffffffffffffff (prio 0, i/o): bus master container

address-space: PIIX3
  0000000000000000-ffffffffffffffff (prio 0, i/o): bus master container

 

2. MemoryRegion

MemoryRegion = 게스트 메모리 맵을 트리로 표현한 것.

게스트의 메모리는 단순히 "RAM 한 덩어리"가 아니라, RAM·ROM·MMIO(장치 레지스터)·PCI 영역 등이 주소 공간 위에 겹겹이 배치된 구조다. 이걸 트리(정확히는 비순환 그래프)로 표현한 게 MemoryRegion이다.

 

  • 리프 노드(말단): 실제 실체가 있는 영역. 진짜 RAM이거나 MMIO(장치) 영역.
  • 중간 노드: 그것들을 담는 컨테이너(버스, 묶음 영역 등). 자기 자신은 실체가 없고 자식들을 모아두는 역할.
  • alias(별칭): 다른 MemoryRegion의 일부를 "다른 주소에서도 보이게" 비추는 노드. 예를 들어 같은 RAM을 두 주소 범위에서 접근하게 할 때 쓴다. (심볼릭 링크 같은 개념)

왜 트리(계층)냐? 그냥 "주소 블록들의 1차원 목록"으로 해도 될 텐데 굳이 부모-자식 트리로 만든 이유는, 영역 안에 영역이 들어가는 중첩·포함 관계와 우선순위(겹칠 때 누가 보이는지) 를 표현해야 하기 때문이다.

그래서 각 노드가 child region(자식 목록), subregions_link(부모 목록에 자신을 끼우는 고리), container(부모 가리키기)를 들고 계층 구조를 이룹니다.

그리고 이 트리(관리용)를 평탄화한 게 앞서 본 FlatView(조회용)이다.

메모리 영역 그래프는 고리가 없어야 한다(자기 자신을 포함하는 순환이 생기면 주소 변환이 무한 루프에 빠지므로).

 

(include/exec/memory.h)

/** MemoryRegion:
 *
 * A struct representing a memory region.
 */
struct MemoryRegion {
    Object parent_obj;

    /* private: */

    /* The following fields should fit in a cache line */
    bool romd_mode;
    bool ram;
    bool subpage;
    bool readonly; /* For RAM regions */
    bool nonvolatile;
    bool rom_device;
    bool flush_coalesced_mmio;
    uint8_t dirty_log_mask;
    bool is_iommu;
    RAMBlock *ram_block;
    Object *owner;
    /* owner as TYPE_DEVICE. Used for re-entrancy checks in MR access hotpath */
    DeviceState *dev;

    const MemoryRegionOps *ops;
    void *opaque;
    MemoryRegion *container;
    int mapped_via_alias; /* Mapped via an alias, container might be NULL */
    Int128 size;
    hwaddr addr;
    void (*destructor)(MemoryRegion *mr);
    uint64_t align;
    bool terminates;
    bool ram_device;
    bool enabled;
    bool warning_printed; /* For reservations */
    uint8_t vga_logging_count;
    MemoryRegion *alias;
    hwaddr alias_offset;
    int32_t priority;
    QTAILQ_HEAD(, MemoryRegion) subregions;
    QTAILQ_ENTRY(MemoryRegion) subregions_link;
    QTAILQ_HEAD(, CoalescedMemoryRange) coalesced;
    const char *name;
    unsigned ioeventfd_nb;
    MemoryRegionIoeventfd *ioeventfds;
    RamDiscardManager *rdm; /* Only for RAM */

    /* For devices designed to perform re-entrant IO into their own IO MRs */
    bool disable_reentrancy_guard;
};

그 중에서 비교적 중요한 멤버는 다음과 같다.

  • *RAMBlock ram_block

ram_block은 실제로 할당된 물리 메모리를 표시한다. 뒤에서 상세히 분석할 것이다.

  • *const MemoryRegionOps ops

ops 안에는 콜백 함수가 들어있는데, MemoryRegion에 대해 조작을 할 때 호출된다. 예를 들어 MMIO의 읽기 쓰기 요청 같은 것이다.

  • *MemoryRegion container

container는 이 MemoryRegion이 처한 상위 레벨 MemoryRegion을 표시한다.

  • hwaddr addr

addr은 이 MemoryRegion이 있는 가상 머신의 물리 주소를 표시한다.

  • bool terminates

terminates는 잎 노드인지 아닌지를 지시하는 데 사용된다.

  • int32_t priority

priority는 MemoryRegion의 우선순위를 지시하는 데 사용된다.

  • QTAILQ_HEAD(, MemoryRegion) subregions

subregions는 이 MemoryRegion에 속한 자 MemoryRegion을 연결한다.

  • QTAILQ_ENTRY(MemoryRegion) subregions_link

subregions_link는 같은 부모 MemoryRegion 아래의 같은 형제를 연결하는 데 사용된다.

 

AddressSpace와 MemoryRegion 관계도

 

MemoryRegion에 대한 추가 설명

  1. MemoryRegion 안에는 기본적으로 hwaddr(주소)과 size(크기)가 있는데, 이는 게스트 시점의 주소 즉 GPA(Guest Physical Address, 게스트 물리 주소) 를 기준으로 한다. 그리고 offset도 하나 있다.
  2. 부모-자식 MemoryRegion 사이는 포함 관계이며, 각 MemoryRegion은 자신이 가진 모든 자식 region의 공간을 아우른다.
  3. 같은 MemoryRegion 아래의 자식들은 나란히 놓이지만 서로 배타적이진 않다. 주소 공간이 겹치는 경우, 우선순위(priority) 에 따라 누가 보일지 결정된다.
  4. 최상위 MemoryRegion은 AddressSpace의 root 변수에 기록된다.
  5. 주의할 점: QEMU가 관리하는 것은 모든 주소 공간이므로, MemoryRegion은 이름이 "Memory"여도 실제로는 메모리만 다루지 않는다. RAM 외에 PCI BAR 공간(memory 타입), config 공간(io/memory 타입) 등도 포함한다. 게스트의 모든 주소 공간 접근은 전부 이 구조를 거쳐 관리된다.

흔한 MemoryRegion의 종류

RAM —  호스트에 실제로 할당되어 게스트에게 물리 메모리로 제공되는 호스트 가상 메모리 한 구간. 메모리(Memory)와 캐시(Cache)가 이 RAM 유형이다.

memory_region_init_ram() —— 통용의
memory_region_init_resizeable_ram()、 memory_region_init_ram_from_file()、memory_region_init_ram_ptr() —— 특수한 것

MMIO — 게스트의 한 메모리 구간이지만 호스트에 대응하는 실제 메모리가 없고, 이 영역에 대한 접근을 가로채서 읽기/쓰기 콜백 함수를 호출한다. 장치 에뮬레이션에 쓰인다. 즉 read/write 할 때마다 호스트의 콜백이 트리거된다.

memory_region_init_io()

ROM — RAM과 비슷하나 읽기 전용. 플래시(예: OS 로더)에 쓰인다.

memory_region_init_rom()

ROM device — 읽기는 RAM처럼, 쓰기는 MMIO처럼(쓰면 콜백 호출).

memory_region_init_rom_device()

IOMMU — 주소를 다른 memory region으로 직접 변환(번역)할 수 있는 region. IOMMU 장치에만 사용. 

memory_region_init_iommu()

container — 여러 MemoryRegion을 담는 그릇. 각 region이 container 안에서 갖는 오프셋이 다르다. 여러 region을 하나의 단위로 묶어 관리할 때 유용하다(예: PCI BAR 하나가 RAM region + MMIO region을 포함). 

memory_region_init()

alias(별칭) — region의 또 다른 일부분이며, 하나의 region을 몇 개의 불연속적인 부분으로 나눌 수 있게 한다.

일반적인 상황에서, MemoryRegion은 겹치지 않으며, 한 주소를 해결할 때, 하나의 MemoryRegion에만 떨어진다.

그리고 어떤 때는, MemoryRegion이 겹치게 하는 것도 비교적 유용하다.
하지만 MemoryRegion이 겹칠 때, 도대체 어느 것이 가상 머신에게 보이게 할지를 결정하는 메커니즘이 필요한데, 이것이 바로 MemoryRegion 구조체 안의 priority의 역할이다.

 

하나의 region의 서브셋이다. 하나의 region을 불연속적인 자 region으로 분해할 수 있도록 허락한다.

memory_region_add_subregion() —— 이미 존재하는 region을 어느 container에 추가하는 데 사용
memory_region_init_alias() —— 새로운 region을 생성하는 데 사용

이상은 구체적인 MemoryRegion 유형으로 구분한 것이고, 더 큰 차원에서는 MemoryRegion을 다음 세 가지로 나눌 수 있다.

1) 루트급(root) MemoryRegion memory_region_init()으로 초기화.

  • 자기 자신의 메모리는 없고, subregion 관리용.
  • 예: system_memory. (= 트리의 뿌리, 컨테이너 역할)

2) 실체(real) MemoryRegion memory_region_init_ram()으로 초기화.

  • 자기 메모리를 실제로 가짐(QEMU 프로세스 주소 공간에서 할당), 크기는 size.
  • 예: ram_memory(pc.ram), pci_memory(pci).
  • 여기서 진짜 물리 메모리를 할당하는 건 pc.ram과 pci이고, 그 용도는 각각 메모리(RAM), PCI 영역 공간, firmware 공간이다.
  • QEMU는 유저 공간 코드라서 할당된 메모리가 돌려주는 주소는 HVA(Host Virtual Address) 이고, 이 HVA는 RAMBlock의 host 필드에 보관된다. 즉 실체 MemoryRegion에 대응하는 RAMBlock을 통해 HVA를 관리한다.

3) 별칭(alias) MemoryRegion memory_region_init_alias()으로 초기화.

  • 자기 메모리는 없고, 실체 MemoryRegion(pc.ram 등)의 일부분을 가리킨다.
  • alias 멤버로 실체 MemoryRegion을 가리키고, alias_offset은 "이 별칭이 가리키는 메모리의 시작 GPA"가 "실체 MemoryRegion의 시작 GPA"로부터 얼마나 떨어져 있는지(오프셋)를 나타낸다.
  • 예: ram_below_4g, ram_above_4g.
종류 자기 메모리 역할 예시
루트급 없음 자식 region 묶는 컨테이너 system_memory
실체 있음 (HVA, RAMBlock에 저장) 진짜 물리 메모리 보유 pc.ram, pci
별칭 없음 실체의 일부를 다른 주소에서 비춤 ram_below_4g

특히 2)번이 RAMBlock과 연결되는 지점이 중요하다.

앞서 RAMBlock을 "실제 할당된 물리 메모리 조각"이라고 했는데, 바로 이 실체 MemoryRegion이 RAMBlock을 통해 실제 메모리(HVA)를 들고 있는 거다. MemoryRegion(주소 공간 트리의 노드) ↔ RAMBlock(실제 메모리 덩어리)의 연결고리가 여기서 드러난다.

 

그리고 3)번 alias의 대표 사례인 ram_below_4g / ram_above_4g 는, 32비트 호환을 위해 4GB 경계에서 RAM을 둘로 쪼개 배치하는 그 유명한 구조다(중간의 PCI 영역 때문에 RAM이 4GB 아래/위로 나뉨).

하나의 실체 RAM(pc.ram)을 두 개의 alias로 서로 다른 GPA 구간에 비춘다.

 

3. RAMBlock

메모리 관리에서 가장 기초적인 부분은 당연히 물리 Memory 메모리이고, 그 외에 또 MMIO 공간, IO 포트의 주소 공간을 포함한다.

RAMBlock 구조가 표시하는 것이 바로 메모리 조각이며, 하나의 RAMBlock은 가상 머신 안의 하나의 메모리 조각에 대응한다.

 

(include/qemu/typedefs.h)

typedef struct RAMBlock RAMBlock;

 

(include/exec/ramblock.h)

struct RAMBlock {
    struct rcu_head rcu;
    struct MemoryRegion *mr;
    uint8_t *host;
    uint8_t *colo_cache; /* For colo, VM's ram cache */
    ram_addr_t offset;
    ram_addr_t used_length;
    ram_addr_t max_length;
    void (*resized)(const char*, uint64_t length, void *host);
    uint32_t flags;
    /* Protected by iothread lock.  */
    char idstr[256];
    /* RCU-enabled, writes protected by the ramlist lock */
    QLIST_ENTRY(RAMBlock) next;
    QLIST_HEAD(, RAMBlockNotifier) ramblock_notifiers;
    int fd;
    uint64_t fd_offset;
    size_t page_size;
    /* dirty bitmap used during migration */
    unsigned long *bmap;
    /* bitmap of already received pages in postcopy */
    unsigned long *receivedmap;

    /*
     * bitmap to track already cleared dirty bitmap.  When the bit is
     * set, it means the corresponding memory chunk needs a log-clear.
     * Set this up to non-NULL to enable the capability to postpone
     * and split clearing of dirty bitmap on the remote node (e.g.,
     * KVM).  The bitmap will be set only when doing global sync.
     *
     * It is only used during src side of ram migration, and it is
     * protected by the global ram_state.bitmap_mutex.
     *
     * NOTE: this bitmap is different comparing to the other bitmaps
     * in that one bit can represent multiple guest pages (which is
     * decided by the `clear_bmap_shift' variable below).  On
     * destination side, this should always be NULL, and the variable
     * `clear_bmap_shift' is meaningless.
     */
    unsigned long *clear_bmap;
    uint8_t clear_bmap_shift;

    /*
     * RAM block length that corresponds to the used_length on the migration
     * source (after RAM block sizes were synchronized). Especially, after
     * starting to run the guest, used_length and postcopy_length can differ.
     * Used to register/unregister uffd handlers and as the size of the received
     * bitmap. Receiving any page beyond this length will bail out, as it
     * could not have been valid on the source.
     */
    ram_addr_t postcopy_length;
};

위에서 이미 언급했듯이, RAMBlock 구조가 표시하는 것은 가상 머신 안의 메모리 조각이며, 하나의 RAMBlock은 가상 머신 안의 하나의 메모리 조각에 대응한다.

RAMBlock 안에는 이 메모리 조각의 일부 기본 정보가 기록되어 있는데, 예를 들면 속하는 mr (struct MemoryRegion *mr), 만약 파일이 백엔드로 있다면 그 파일에 대응하는 fd (int fd), 시스템의 페이지 크기 page_size (size_t page_size), 이미 사용된 크기 used_length (ram_addr_t used_length), 이 메모리 조각이 가상 머신 전체 메모리 안에서의 오프셋 offset (ram_addr_t offset) 등이 있다.

각 MemoryRegion 안에는 RAMBlock의 포인터가 하나씩 포함되어 있지만, 반드시 RAMBlock에 대응하는 것은 아니다.

물리 메모리에 대해서, 그 실체 MemoryRegion은 하나의 실체 RAMBlock을 가리킨다.

 

(include/exec/memory.h)

/** MemoryRegion:
 *
 * A struct representing a memory region.
 */
struct MemoryRegion {
    Object parent_obj;

    /* private: */

    /* The following fields should fit in a cache line */
    bool romd_mode;
    bool ram;
    bool subpage;
    bool readonly; /* For RAM regions */
    bool nonvolatile;
    bool rom_device;
    bool flush_coalesced_mmio;
    uint8_t dirty_log_mask;
    bool is_iommu;
    RAMBlock *ram_block;
    ......
};
  • *RAMBlock ram_block

ram_block은 실제로 분배된 물리 메모리를 표시한다.

struct RAMBlock의 정의로 돌아가자. 그 안의 주요 논리 변수는 offset (ram_addr_t offset) (GPA)과 host (uint8_t *host) (HVA)다. 또 bmap (unsigned long *bmap)과 receivedmap (unsigned long *receivedmap)이 있는데, 이는 핫 마이그레이션 더티 페이지에 사용된다.

 

그 외에, 모든 RAMBlock은 next (QLIST_ENTRY(RAMBlock) next) 필드를 통해 하나의 링크 테이블에 연결되며, 링크 헤드는 ram_list.blocks 전역 변수다.

 

(include/exec/cpu-common.h)

/* address in the RAM (different from a physical address) */
#if defined(CONFIG_XEN_BACKEND)
typedef uint64_t ram_addr_t;
#  define RAM_ADDR_MAX UINT64_MAX
#  define RAM_ADDR_FMT "%" PRIx64
#else
typedef uintptr_t ram_addr_t;
#  define RAM_ADDR_MAX UINTPTR_MAX
#  define RAM_ADDR_FMT "%" PRIxPTR
#endif
#if __riscv_xlen == 64
typedef long            s64;
typedef unsigned long   u64;
typedef long            int64_t;
typedef unsigned long   uint64_t;
#define PRILX           "016lx"
#elif __riscv_xlen == 32
typedef long long       s64;
typedef unsigned long long  u64;
typedef long long       int64_t;
typedef unsigned long long  uint64_t;
#define PRILX           "08lx"
#else
#error Unexpected __riscv_xlen
#endif
typedef unsigned long       uintptr_t;

 

 

4. FlatView 와 FlatRange

(include/exec/memory.h)

/* Flattened global view of current active memory hierarchy.  Kept in sorted
 * order.
 */
struct FlatView {
    struct rcu_head rcu;
    unsigned ref;
    FlatRange *ranges;
    unsigned nr;
    unsigned nr_allocated;
    struct AddressSpaceDispatch *dispatch;
    MemoryRegion *root;
};

FlatView 구조는 트리 형태의 메모리 구조를 flat 메모리 구조로 변환한 결과다.

왜냐하면 트리 형태의 메모리 구조는 관리하기 비교적 쉽지만, flat 형태의 메모리 구조는 커널과 통신할 때 물리 메모리를 요청하기에 더 편리하기 때문이다.

운영체제 커널 내부에서도 트리 구조로 메모리 영역을 표현하기는 하지만, 유저 모드에서 커널에 메모리를 요청할 때는 평면적이고 연속적인 모드로 요청을 보낸다. 그렇다면 QEMU도 유저 모드에 있으니 평면적이고 연속적인 모드로 요청을 보내야만 한다. 그래서 이런 변환이 반드시 필요한 것이다.

 

가상머신 메모리의 평면화는 AddressSpace를 단위로 한다. 즉, AddressSpace의 루트 MemoryRegion을 출발점으로 해서, 그것이 표현하는 메모리 토폴로지의 비순환 그래프 구조를 평면 모드로 변환한다.

구체적인 구현 함수는 address_space_update_topology와 그 안에 있는 generate_memory_topology이고, 둘 다 softmmu/memory.c에 위치해있다.

static void address_space_update_topology(AddressSpace *as)
{
    MemoryRegion *physmr = memory_region_get_flatview_root(as->root);

    flatviews_init();
    if (!g_hash_table_lookup(flat_views, physmr)) {
        generate_memory_topology(physmr);
    }
    address_space_set_flatview(as);
}
/* Render a memory topology into a list of disjoint absolute ranges. */
static FlatView *generate_memory_topology(MemoryRegion *mr)
{
    int i;
    FlatView *view;

    view = flatview_new(mr);

    if (mr) {
        render_memory_region(view, mr, int128_zero(),
                             addrrange_make(int128_zero(), int128_2_64()),
                             false, false);
    }
    flatview_simplify(view);

    view->dispatch = address_space_dispatch_new(view);
    for (i = 0; i < view->nr; i++) {
        MemoryRegionSection mrs =
            section_from_flat_range(&view->ranges[i], view);
        flatview_add_to_dispatch(view, &mrs);
    }
    address_space_dispatch_compact(view->dispatch);
    g_hash_table_replace(flat_views, mr, view);

    return view;
}

코드 측면에서 종합해보면, 가상머신 메모리의 평면화 과정은 AddressSpace 안의 루트 MemoryRegion이 표현하는 가상머신 메모리 주소 공간을 하나의 평탄한 선형 주소 공간으로 바꾸는 것이다.

각 선형 주소 공간 구간의 속성은 그것이 속한 MemoryRegion과 일치하며, 각 선형 주소 공간 구간은 가상머신의 물리 주소 공간과 서로 매핑된다.

가상머신 메모리의 평면화는 AddressSpace를 단위로 하고, 다시 말하면 AddressSpace의 루트 MemoryRegion을 출발점으로 해서, 그것이 표현하는 메모리 토폴로지의 비순환 그래프 구조를 평면 모드로 변환한다.

 

FlatRange와 FlatView의 관계

앞에서 이야기했듯이, AddressSpace 구조체 안에는 current_map이라는 FlatView 타입의 멤버가 있는데, 이건 해당 AddressSpace에 대응하는 평면 시각(view)을 나타낸다.

/**
 * struct AddressSpace: describes a mapping of addresses to #MemoryRegion objects
 */
struct AddressSpace {
    /* private: */
    struct rcu_head rcu;
    char *name;
    MemoryRegion *root;

    /* Accessed via RCU.  */
    struct FlatView *current_map;   /* ← 평면 시각 */

    int ioeventfd_nb;
    struct MemoryRegionIoeventfd *ioeventfds;
    QTAILQ_HEAD(, MemoryListener) listeners;
    QTAILQ_ENTRY(AddressSpace) address_spaces_link;
};

 

그리고 MemoryRegion이 펼쳐진 뒤의 메모리 토폴로지는 FlatRange로 표현된다.

/* Range of memory in the global map.  Addresses are absolute. */
struct FlatRange {
    MemoryRegion *mr;
    hwaddr offset_in_region;
    AddrRange addr;
    uint8_t dirty_log_mask;
    bool romd_mode;
    bool readonly;
    bool nonvolatile;
};

각각의 FlatRange는 AddressSpace 안의 한 공간 구간을 나타낸다.

 

struct FlatView의 멤버

  • unsigned nr: FlatRange의 개수를 나타낸다.
  • unsigned nr_allocated: 이미 할당된 FlatRange의 개수를 나타낸다.
  • FlatRange *ranges: 할당된 FlatRange 배열을 가리킨다.
  • unsigned ref: 참조 카운트. 0이 되면 소멸시킨다.

struct FlatRange의 멤버

  • MemoryRegion mr: 대응하는 MemoryRegion을 나타낸다.
  • hwaddr offset_in_region: 이 FlatRange가 MemoryRegion 안에서의 오프셋을 나타낸다.
  • AddrRange addr: 이 FlatRange가 나타내는 구간이며, 주소와 크기를 모두 포함한다.
/* Range of memory in the global map.  Addresses are absolute. */
struct FlatRange {
    MemoryRegion *mr;
    hwaddr offset_in_region;
    AddrRange addr;
    uint8_t dirty_log_mask;
    bool romd_mode;
    bool readonly;
    bool nonvolatile;
};

 

FlatView와 FlatRange의 관계

generate_memory_topology 함수가 바로 이런 FlatView를 만들어내는 함수다.

 

3) 메모리 초기화 메인 라인

전체 호출 경로

 

(1) pc_init1 함수에서의 메모리 분할

 

QEMU 가상머신 메모리 초기화

메모리는 가상머신의 기초로서, 그 초기화는 pc_init1 함수에서 진행된다. 

(hw/i386/pc_piix.c)

/* PC hardware initialisation */
static void pc_init1(MachineState *machine,
                     const char *host_type, const char *pci_type)
{
    PCMachineState *pcms = PC_MACHINE(machine);
    PCMachineClass *pcmc = PC_MACHINE_GET_CLASS(pcms);
    X86MachineState *x86ms = X86_MACHINE(machine);
    MemoryRegion *system_memory = get_system_memory();
    MemoryRegion *system_io = get_system_io();
    PCIBus *pci_bus = NULL;
    ISABus *isa_bus;
    int piix3_devfn = -1;
    qemu_irq smi_irq;
    GSIState *gsi_state;
    BusState *idebus[MAX_IDE_BUS];
    ISADevice *rtc_state;
    MemoryRegion *ram_memory;
    MemoryRegion *pci_memory = NULL;
    MemoryRegion *rom_memory = system_memory;
    ram_addr_t lowmem;
    uint64_t hole64_size = 0;

    /*
     * Calculate ram split, for memory below and above 4G.  It's a bit
     * complicated for backward compatibility reasons ...
     *
     *  - Traditional split is 3.5G (lowmem = 0xe0000000).  This is the
     *    default value for max_ram_below_4g now.
     *
     *  - Then, to gigabyte align the memory, we move the split to 3G
     *    (lowmem = 0xc0000000).  But only in case we have to split in
     *    the first place, i.e. ram_size is larger than (traditional)
     *    lowmem.  And for new machine types (gigabyte_align = true)
     *    only, for live migration compatibility reasons.
     *
     *  - Next the max-ram-below-4g option was added, which allowed to
     *    reduce lowmem to a smaller value, to allow a larger PCI I/O
     *    window below 4G.  qemu doesn't enforce gigabyte alignment here,
     *    but prints a warning.
     *
     *  - Finally max-ram-below-4g got updated to also allow raising lowmem,
     *    so legacy non-PAE guests can get as much memory as possible in
     *    the 32bit address space below 4G.
     *
     *  - Note that Xen has its own ram setup code in xen_ram_init(),
     *    called via xen_hvm_init_pc().
     *
     * Examples:
     *    qemu -M pc-1.7 -m 4G    (old default)    -> 3584M low,  512M high
     *    qemu -M pc -m 4G        (new default)    -> 3072M low, 1024M high
     *    qemu -M pc,max-ram-below-4g=2G -m 4G     -> 2048M low, 2048M high
     *    qemu -M pc,max-ram-below-4g=4G -m 3968M  -> 3968M low (=4G-128M)
     */
    if (xen_enabled()) {
        xen_hvm_init_pc(pcms, &ram_memory);
    } else {
        ram_memory = machine->ram;
        if (!pcms->max_ram_below_4g) {
            pcms->max_ram_below_4g = 0xe0000000; /* default: 3.5G */
        }
        lowmem = pcms->max_ram_below_4g;
        if (machine->ram_size >= pcms->max_ram_below_4g) {
            if (pcmc->gigabyte_align) {
                if (lowmem > 0xc0000000) {
                    lowmem = 0xc0000000;
                }
                if (lowmem & (1 * GiB - 1)) {
                    warn_report("Large machine and max_ram_below_4g "
                                "(%" PRIu64 ") not a multiple of 1G; "
                                "possible bad performance.",
                                pcms->max_ram_below_4g);
                }
            }
        }

        if (machine->ram_size >= lowmem) {
            x86ms->above_4g_mem_size = machine->ram_size - lowmem;
            x86ms->below_4g_mem_size = lowmem;
        } else {
            x86ms->above_4g_mem_size = 0;
            x86ms->below_4g_mem_size = machine->ram_size;
        }
    }

    pc_machine_init_sgx_epc(pcms);
    x86_cpus_init(x86ms, pcmc->default_cpu_version);

    if (pcmc->kvmclock_enabled) {
        kvmclock_create(pcms->kvmclock_create_always);
    }

    if (pcmc->pci_enabled) {
        Object *phb;

        pci_memory = g_new(MemoryRegion, 1);
        memory_region_init(pci_memory, NULL, "pci", UINT64_MAX);
        rom_memory = pci_memory;

        phb = OBJECT(qdev_new(host_type));
        object_property_add_child(OBJECT(machine), "i440fx", phb);
        object_property_set_link(phb, PCI_HOST_PROP_RAM_MEM,
                                 OBJECT(ram_memory), &error_fatal);
        object_property_set_link(phb, PCI_HOST_PROP_PCI_MEM,
                                 OBJECT(pci_memory), &error_fatal);
        object_property_set_link(phb, PCI_HOST_PROP_SYSTEM_MEM,
                                 OBJECT(system_memory), &error_fatal);
        object_property_set_link(phb, PCI_HOST_PROP_IO_MEM,
                                 OBJECT(system_io), &error_fatal);
        object_property_set_uint(phb, PCI_HOST_BELOW_4G_MEM_SIZE,
                                 x86ms->below_4g_mem_size, &error_fatal);
        object_property_set_uint(phb, PCI_HOST_ABOVE_4G_MEM_SIZE,
                                 x86ms->above_4g_mem_size, &error_fatal);
        object_property_set_str(phb, I440FX_HOST_PROP_PCI_TYPE, pci_type,
                                &error_fatal);
        sysbus_realize_and_unref(SYS_BUS_DEVICE(phb), &error_fatal);

        pci_bus = PCI_BUS(qdev_get_child_bus(phb, "pci.0"));
        pci_bus_map_irqs(pci_bus,
                         xen_enabled() ? xen_pci_slot_get_pirq
                                       : pc_pci_slot_get_pirq);
        pcms->bus = pci_bus;

        hole64_size = object_property_get_uint(phb,
                                               PCI_HOST_PROP_PCI_HOLE64_SIZE,
                                               &error_abort);
    }

    pc_guest_info_init(pcms);

    if (pcmc->smbios_defaults) {
        MachineClass *mc = MACHINE_GET_CLASS(machine);
        /* These values are guest ABI, do not change */
        smbios_set_defaults("QEMU", mc->desc,
                            mc->name, pcmc->smbios_legacy_mode,
                            pcms->smbios_uuid_encoded,
                            pcms->smbios_entry_point_type);
    }

    /* allocate ram and load rom/bios */
    if (!xen_enabled()) {
        pc_memory_init(pcms, system_memory, rom_memory, hole64_size);
    } else {
        assert(machine->ram_size == x86ms->below_4g_mem_size +
                                    x86ms->above_4g_mem_size);

        pc_system_flash_cleanup_unused(pcms);
        if (machine->kernel_filename != NULL) {
            /* For xen HVM direct kernel boot, load linux here */
            xen_load_linux(pcms);
        }
    }

    gsi_state = pc_gsi_create(&x86ms->gsi, pcms->pci_enabled);

    if (pcmc->pci_enabled) {
        PIIX3State *piix3;
        PCIDevice *pci_dev;

        pci_dev = pci_create_simple_multifunction(pci_bus, -1, TYPE_PIIX3_DEVICE);

        if (xen_enabled()) {
            pci_device_set_intx_routing_notifier(
                        pci_dev, piix_intx_routing_notifier_xen);

            /*
             * Xen supports additional interrupt routes from the PCI devices to
             * the IOAPIC: the four pins of each PCI device on the bus are also
             * connected to the IOAPIC directly.
             * These additional routes can be discovered through ACPI.
             */
            pci_bus_irqs(pci_bus, xen_intx_set_irq, pci_dev,
                         XEN_IOAPIC_NUM_PIRQS);
        }

        piix3 = PIIX3_PCI_DEVICE(pci_dev);
        piix3->pic = x86ms->gsi;
        piix3_devfn = piix3->dev.devfn;
        isa_bus = ISA_BUS(qdev_get_child_bus(DEVICE(piix3), "isa.0"));
        rtc_state = ISA_DEVICE(object_resolve_path_component(OBJECT(pci_dev),
                                                             "rtc"));
    } else {
        isa_bus = isa_bus_new(NULL, system_memory, system_io,
                              &error_abort);

        rtc_state = isa_new(TYPE_MC146818_RTC);
        qdev_prop_set_int32(DEVICE(rtc_state), "base_year", 2000);
        isa_realize_and_unref(rtc_state, isa_bus, &error_fatal);

        i8257_dma_init(isa_bus, 0);
        pcms->hpet_enabled = false;
    }

    isa_bus_register_input_irqs(isa_bus, x86ms->gsi);

    if (x86ms->pic == ON_OFF_AUTO_ON || x86ms->pic == ON_OFF_AUTO_AUTO) {
        pc_i8259_create(isa_bus, gsi_state->i8259_irq);
    }

    if (pcmc->pci_enabled) {
        ioapic_init_gsi(gsi_state, "i440fx");
    }

    if (tcg_enabled()) {
        x86_register_ferr_irq(x86ms->gsi[13]);
    }

    pc_vga_init(isa_bus, pcmc->pci_enabled ? pci_bus : NULL);

    assert(pcms->vmport != ON_OFF_AUTO__MAX);
    if (pcms->vmport == ON_OFF_AUTO_AUTO) {
        pcms->vmport = xen_enabled() ? ON_OFF_AUTO_OFF : ON_OFF_AUTO_ON;
    }

    /* init basic PC hardware */
    pc_basic_device_init(pcms, isa_bus, x86ms->gsi, rtc_state, true,
                         0x4);

    pc_nic_init(pcmc, isa_bus, pci_bus);

    if (pcmc->pci_enabled) {
        PCIDevice *dev;

        dev = pci_create_simple(pci_bus, piix3_devfn + 1, TYPE_PIIX3_IDE);
        pci_ide_create_devs(dev);
        idebus[0] = qdev_get_child_bus(&dev->qdev, "ide.0");
        idebus[1] = qdev_get_child_bus(&dev->qdev, "ide.1");
        pc_cmos_init(pcms, idebus[0], idebus[1], rtc_state);
#ifdef CONFIG_IDE_ISA
    } else {
        DriveInfo *hd[MAX_IDE_BUS * MAX_IDE_DEVS];
        int i;

        ide_drive_get(hd, ARRAY_SIZE(hd));
        for (i = 0; i < MAX_IDE_BUS; i++) {
            ISADevice *dev;
            char busname[] = "ide.0";
            dev = isa_ide_init(isa_bus, ide_iobase[i], ide_iobase2[i],
                               ide_irq[i],
                               hd[MAX_IDE_DEVS * i], hd[MAX_IDE_DEVS * i + 1]);
            /*
             * The ide bus name is ide.0 for the first bus and ide.1 for the
             * second one.
             */
            busname[4] = '0' + i;
            idebus[i] = qdev_get_child_bus(DEVICE(dev), busname);
        }
        pc_cmos_init(pcms, idebus[0], idebus[1], rtc_state);
    }
#endif

    if (pcmc->pci_enabled && machine_usb(machine)) {
        pci_create_simple(pci_bus, piix3_devfn + 2, TYPE_PIIX3_USB_UHCI);
    }

    if (pcmc->pci_enabled && x86_machine_is_acpi_enabled(X86_MACHINE(pcms))) {
        PCIDevice *piix4_pm;

        smi_irq = qemu_allocate_irq(pc_acpi_smi_interrupt, first_cpu, 0);
        piix4_pm = pci_new(piix3_devfn + 3, TYPE_PIIX4_PM);
        qdev_prop_set_uint32(DEVICE(piix4_pm), "smb_io_base", 0xb100);
        qdev_prop_set_bit(DEVICE(piix4_pm), "smm-enabled",
                          x86_machine_is_smm_enabled(x86ms));
        pci_realize_and_unref(piix4_pm, pci_bus, &error_fatal);

        qdev_connect_gpio_out(DEVICE(piix4_pm), 0, x86ms->gsi[9]);
        qdev_connect_gpio_out_named(DEVICE(piix4_pm), "smi-irq", 0, smi_irq);
        pcms->smbus = I2C_BUS(qdev_get_child_bus(DEVICE(piix4_pm), "i2c"));
        /* TODO: Populate SPD eeprom data. */
        smbus_eeprom_init(pcms->smbus, 8, NULL, 0);

        object_property_add_link(OBJECT(machine), PC_MACHINE_ACPI_DEVICE_PROP,
                                 TYPE_HOTPLUG_HANDLER,
                                 (Object **)&x86ms->acpi_dev,
                                 object_property_allow_set_link,
                                 OBJ_PROP_LINK_STRONG);
        object_property_set_link(OBJECT(machine), PC_MACHINE_ACPI_DEVICE_PROP,
                                 OBJECT(piix4_pm), &error_abort);
    }

    if (machine->nvdimms_state->is_enabled) {
        nvdimm_init_acpi_state(machine->nvdimms_state, system_io,
                               x86_nvdimm_acpi_dsmio,
                               x86ms->fw_cfg, OBJECT(pcms));
    }
}

 

low / high 메모리 분할의 개념

메모리는 low 메모리와 high 메모리로 나뉜다. 이렇게 구분하는 이유는 일부 전통적인 장치를 가진 가상머신의 경우, 그 장치들이 반드시 4GB 이하의 메모리 주소 공간을 사용해야 하기 때문이다.

ram_memory = machine->ram;
if (!pcms->max_ram_below_4g) {
    pcms->max_ram_below_4g = 0xe0000000; /* default: 3.5G */
}
lowmem = pcms->max_ram_below_4g;
if (machine->ram_size >= pcms->max_ram_below_4g) {
    if (pcmc->gigabyte_align) {
        if (lowmem > 0xc0000000) {
            lowmem = 0xc0000000;
        }
        if (lowmem & (1 * GiB - 1)) {
            warn_report("Large machine and max_ram_below_4g "
                        "(%" PRIu64 ") not a multiple of 1G; "
                        "possible bad performance.",
                        pcms->max_ram_below_4g);
        }
    }
}

if (machine->ram_size >= lowmem) {
    x86ms->above_4g_mem_size = machine->ram_size - lowmem;
    x86ms->below_4g_mem_size = lowmem;
} else {
    x86ms->above_4g_mem_size = 0;
    x86ms->below_4g_mem_size = machine->ram_size;
}

 

분할 결정 로직

만약 사용자가 커맨드라인에서 max-ram-below-4g 파라미터를 지정했다면, 사용자가 지정한 파라미터를 사용한다.

이는 일부 비전통적인 가상머신이 더 많은 4GB 이하 주소 공간을 사용할 수 있게 해준다.

사용자가 max-ram-below-4g 파라미터를 지정하지 않았을 경우에는 다시 두 가지 상황으로 나뉜다.

 

1) 전통적인 가상머신(qemu-2.5 버전 이하)은 3.5GB(0xe0000000)를 분기선으로 사용한다.

ram_memory = machine->ram;
if (!pcms->max_ram_below_4g) {
    pcms->max_ram_below_4g = 0xe0000000; /* default: 3.5G */
}
lowmem = pcms->max_ram_below_4g;

 

2) 높은 버전의 가상머신 하에서는 gigabyte_align을 설정한다. 가상머신의 메모리가 전통적인 low 메모리(3.5GB)보다 클 때는 3GB(0xc0000000)를 분기선으로 삼는다.

if (machine->ram_size >= pcms->max_ram_below_4g) {
    if (pcmc->gigabyte_align) {
        if (lowmem > 0xc0000000) {
            lowmem = 0xc0000000;
        }
        if (lowmem & (1 * GiB - 1)) {
            warn_report("Large machine and max_ram_below_4g "
                        "(%" PRIu64 ") not a multiple of 1G; "
                        "possible bad performance.",
                        pcms->max_ram_below_4g);
        }
    }
}

lowmem을 초과하는 주소는 high 메모리고, 그렇지 않으면 low 메모리다.

if (machine->ram_size >= lowmem) {
    x86ms->above_4g_mem_size = machine->ram_size - lowmem;
    x86ms->below_4g_mem_size = lowmem;
} else {
    x86ms->above_4g_mem_size = 0;
    x86ms->below_4g_mem_size = machine->ram_size;
}

 

(2) cpu_exec_init_all - io_mem_init과 memory_map_init

메모리는 가상머신의 기초 부분으로서, 그 초기화는 pc_init1 함수에서 일부 작업을 수행하는 것 외에도, cpu_exec_init_all 함수에서도 일부 초기화 작업을 진행한다.

(softmmu/physmem.c)

void cpu_exec_init_all(void)
{
    qemu_mutex_init(&ram_list.mutex);
    /* The data structures we set up here depend on knowing the page size,
     * so no more changes can be made after this point.
     * In an ideal world, nothing we did before we had finished the
     * machine setup would care about the target page size, and we could
     * do this much later, rather than requiring board models to state
     * up front what their requirements are.
     */
    finalize_target_page_bits();
    io_mem_init();
    memory_map_init();
    qemu_mutex_init(&map_client_list_lock);
}

cpu_exec_init_all 함수에는 메모리와 관련된 두 개의 함수 호출이 있다. 이름만 봐도 알 수 있듯이, io_mem_init()과 memory_map_init()이다. 

 

io_mem_init 함수든 memory_map_init 함수든, 그 밑단에서는 직접 또는 간접적으로 memory_region_init 함수를 호출한다.

 

io_mem_init 함수 (softmmu/physmem.c)

static void io_mem_init(void)
{
    memory_region_init_io(&io_mem_unassigned, NULL, &unassigned_mem_ops, NULL,
                          NULL, UINT64_MAX);
}

 

void memory_region_init_io(MemoryRegion *mr,
                           Object *owner,
                           const MemoryRegionOps *ops,
                           void *opaque,
                           const char *name,
                           uint64_t size)
{
    memory_region_init(mr, owner, name, size);
    mr->ops = ops ? ops : &unassigned_mem_ops;
    mr->opaque = opaque;
    mr->terminates = true;
}

 

io_mem_init 함수는 비교적 간단하다. 그 기능은 모든 주소 공간을 포함하는 여러 개의 MemoryRegion을 생성하는 것이다. 예를 들면 io_mem_rom과 io_mem_unassigned 같은 것들이다. 좀 더 직관적으로 말하자면, 이 함수는 I/O 저장기의 초기화를 완료해서 시스템 전체에 통일된 메모리 접근 영역을 만들어 준다.

 

memory_map_init 함수 (softmmu/physmem.c)

static void memory_map_init(void)
{
    system_memory = g_malloc(sizeof(*system_memory));

    memory_region_init(system_memory, NULL, "system", UINT64_MAX);
    address_space_init(&address_space_memory, system_memory, "memory");

    system_io = g_malloc(sizeof(*system_io));
    memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io",
                          65536);
    address_space_init(&address_space_io, system_io, "I/O");
}

memory_map_init 함수는 중요한 함수다. 시스템 메모리 주소 공간과 I/O 주소 공간에 대한 매핑을 수행해서, 이후 장치가 접근할 수 있도록 준비를 한다.

memory_map_init 함수에서도 memory_region_init 함수를 호출하는데(물론 memory_region_init_io 함수도 호출한다), 다만 io_mem_init 함수에서 (간접적으로) 호출한 것은 다음과 같고

memory_region_init(&io_mem_unassigned, NULL, NULL, UINT64_MAX);

memory_map_init 함수에서 호출하는 것은 다음과 같다.

memory_region_init(system_memory, NULL, "system", UINT64_MAX);

 

AddressSpace와 MemoryRegion 관계

memory_map_init 함수에서는 두 개의 AddressSpace를 생성한다. address_space_memory와 address_space_io로, 각각 가상머신의 메모리 주소 공간과 I/O 주소 공간을 나타낸다. 거기에 대응하는 루트 MemoryRegion은 각각 system_memory와 system_io다.

즉, memory_map_init 함수에서, 시스템 메모리 영역 system_memory와 I/O용 메모리 영역 system_io에 대해 모두 초기화를 수행하고, 그에 상응하는 주소 공간 AddressSpace와 연결한다.

 

이 두 AddressSpace(address_space_memory와 address_space_io)와 대응하는 루트 MemoryRegion은 모두 전역 변수라, 시스템에서 여러 곳에서 사용된다.

address_space_memory, address_space_io 그리고 system_memory, system_io는 모두 softmmu/physmem.c에서 정의돼 있고, 코드는 다음과 같다.

static MemoryRegion *system_memory;
static MemoryRegion *system_io;

AddressSpace address_space_io;
AddressSpace address_space_memory;

 

(3) memory_region_init 함수 내부 파헤치기

이번에는 memory_region_init 함수의 소스코드를 살펴보면서, 관련된 몇 가지 자료 구조체에 대해 좀 더 종합적으로 파악해 본다.

memory_region_init 함수는 softmmu/memory.c에 있고, 코드는 다음과 같다.

void memory_region_init(MemoryRegion *mr,
                        Object *owner,
                        const char *name,
                        uint64_t size)
{
    object_initialize(mr, sizeof(*mr), TYPE_MEMORY_REGION);
    memory_region_do_init(mr, owner, name, size);
}

앞서 얘기했듯이, memory_map_init 함수에서 두 개의 AddressSpace를 생성한다. address_space_memory와 address_space_io로, 각각 가상머신의 메모리 주소 공간과 I/O 주소 공간을 나타낸다. 거기에 대응하는 루트 MemoryRegion은 각각 system_memory와 system_io다.

 

즉, 두 번 호출되는 memory_region_init 함수에 전달되는 실제 인자는 각각 system_memory(가상머신의 메모리 주소 공간에 대응하는 루트 MemoryRegion)와 system_io(가상머신의 I/O 주소 공간에 대응하는 루트 MemoryRegion)다.

다시 memory_region_init 함수 코드로 돌아가자.

void memory_region_init(MemoryRegion *mr,
                        Object *owner,
                        const char *name,
                        uint64_t size)
{
    object_initialize(mr, sizeof(*mr), TYPE_MEMORY_REGION);
    memory_region_do_init(mr, owner, name, size);
}

memory_region_init 함수는 모두 두 개의 함수를 호출한다. 하나씩 살펴보자.

 

1) object_initialize 함수 (qom\object.c)

object_initialize(mr, sizeof(*mr), TYPE_MEMORY_REGION);

 

void object_initialize(void *data, size_t size, const char *typename)
{
    TypeImpl *type = type_get_by_name(typename);

#ifdef CONFIG_MODULES
    if (!type) {
        int rv = module_load_qom(typename, &error_fatal);
        if (rv > 0) {
            type = type_get_by_name(typename);
        } else {
            error_report("missing object type '%s'", typename);
            exit(1);
        }
    }
#endif
    if (!type) {
        error_report("missing object type '%s'", typename);
        abort();
    }

    object_initialize_with_type(data, size, type);
}

 

object_initialize 함수는 QOM(QEMU Object Model)과 밀접하게 관련돼 있다. 

 

TYPE_MEMORY_REGION 매크로 (include\exec\memory.h)

#define TYPE_MEMORY_REGION "memory-region"

 

2) memory_region_do_init 함수 (softmmu/memory.c)

memory_region_do_init(mr, owner, name, size);

 

static void memory_region_do_init(MemoryRegion *mr,
                                  Object *owner,
                                  const char *name,
                                  uint64_t size)
{
    mr->size = int128_make64(size);
    if (size == UINT64_MAX) {
        mr->size = int128_2_64();
    }
    mr->name = g_strdup(name);
    mr->owner = owner;
    mr->dev = (DeviceState *) object_dynamic_cast(mr->owner, TYPE_DEVICE);
    mr->ram_block = NULL;

    if (name) {
        char *escaped_name = memory_region_escape_name(name);
        char *name_array = g_strdup_printf("%s[*]", escaped_name);

        if (!owner) {
            owner = container_get(qdev_get_machine(), "/unattached");
        }

        object_property_add_child(owner, name_array, OBJECT(mr));
        object_unref(OBJECT(mr));
        g_free(name_array);
        g_free(escaped_name);
    }
}

 

(4) address_space_init과 주소 공간 토폴로지 갱신

address_space_init 함수의 코드, 즉 주소 공간 초기화 및 메모리 영역과의 연결 코드를 살펴보자.

 

(softmmu/memory.c)

void address_space_init(AddressSpace *as, MemoryRegion *root, const char *name)
{
    memory_region_ref(root);
    as->root = root;
    as->current_map = NULL;
    as->ioeventfd_nb = 0;
    as->ioeventfds = NULL;
    QTAILQ_INIT(&as->listeners);
    QTAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link);
    as->name = g_strdup(name ? name : "anonymous");
    address_space_update_topology(as);
    address_space_update_ioeventfds(as);
}

 

시스템 메모리 주소 공간 address_space_memory의 경우, 그 메모리 영역의 루트 root를 system_memory로 설정해야 한다.

 

AddressSpace 초기화 후의 구조

address_space_init 함수가 호출된 직후의 AddressSpace 구조는 다음과 같다.

 

QTAILQ_INIT 매크로 (include/qemu/queue.h)

/*
 * Tail queue functions.
 */
#define QTAILQ_INIT(head) do {                          \
        (head)->tqh_first = NULL;                       \
        (head)->tqh_circ.tql_prev = &(head)->tqh_circ;  \
} while (/*CONSTCOND*/0)

여기서 head에 대응되는 실제 인자는 &as->listeners다. 그러면 실제로는 다음과 같다.

do {                                                                  \
    (&as->listeners)->tqh_first = NULL;                               \
    (&as->listeners)->tqh_circ.tql_prev = &(&as->listeners)->tqh_circ;\
} while (/*CONSTCOND*/0)

 

QTAILQ_INSERT_TAIL 매크로 (include/qemu/queue.h)

#define QTAILQ_INSERT_TAIL(head, elm, field) do {                       \
        (elm)->field.tqe_next = NULL;                                   \
        (elm)->field.tqe_circ.tql_prev = (head)->tqh_circ.tql_prev;     \
        (head)->tqh_circ.tql_prev->tql_next = (elm);                    \
        (head)->tqh_circ.tql_prev = &(elm)->field.tqe_circ;             \
} while (/*CONSTCOND*/0)

 

address_space_init 함수에서는 이렇게 호출한다.

QTAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link);

따라서 전개하면 다음과 같다.

do {                                                                                \
    (as)->address_spaces_link.tqe_next = NULL;                                      \
    (as)->address_spaces_link.tqe_circ.tql_prev = (&address_spaces)->tqh_circ.tql_prev; \
    (&address_spaces)->tqh_circ.tql_prev->tql_next = (as);                          \
    (&address_spaces)->tqh_circ.tql_prev = &(as)->address_spaces_link.tqe_circ;     \
} while (/*CONSTCOND*/0)

여기서 as가 가리키는 것은 address_space_memory다.

 

void address_space_init(AddressSpace *as, MemoryRegion *root, const char *name)

address_spaces의 정의는 softmmu/memory.c에 있고, 아래와 같다.

static QTAILQ_HEAD(, AddressSpace) address_spaces
    = QTAILQ_HEAD_INITIALIZER(address_spaces);

 

이어지는 부분은 address_space_init 함수의 두 가지 중요한 함수다.

address_space_update_topology(as);
address_space_update_ioeventfds(as);

 

QTAILQ 자료구조 시각화

 

4) 메모리 평탄화 (트리 → 평면)

트리 구조(MemoryRegion)를 커널과 통신하기 좋은 평면 구조(FlatView)로 변환하는 과정이다. address_space_update_topology → generate_memory_topology → render_memory_region(재귀) → flatview_simplify 순으로 진행되며, 생성된 FlatView는 전역 해시 테이블 flat_views에 등록된다.

 

1. address_space_update_topology 진입과 트리 vs 평면

address_space_update_topology 함수

시스템 메모리 영역의 루트를 설정한 다음에는, address_space_update_topology 함수를 호출해서 메모리 토폴로지를 갱신해야 한다.

 

address_space_update_topology 함수에서는 AddressSpace의 flatview를 생성한다. 여기서 메모리는 초기화 단계에 있고, 첫 번째 구축에 해당하기 때문에 generate_memory_topology 함수가 실행되어 처음으로 flat_views를 생성한다.

 

트리 구조 vs 평면 구조

이 flatview(코드에서는 flat_views) 구조체가 나타내는 것은 주소 공간(AddressSpace)이 트리 구조(MemoryRegion)와 병렬을 이루는 "평면" 구조다.

왜 두 가지 구조로 같은 것을 표현해야 할까? 트리 형태의 메모리 구조는 관리하기 더 편하기 때문이고, 평면 메모리 구조는 커널과 통신해서 물리 메모리를 할당하는 데 더 편리하기 때문이다. 비록 운영체제 커널에서도 트리 구조를 사용해 메모리 영역을 나타내지만, 사용자 모드에서 커널에 메모리를 요청할 때는 평면적이고 연속적인 모드로 할당받게 된다. QEMU는 사용자 모드에 있으니, 이런 변환을 한 번 해줘야 하는 것이다.

 

2. flatview 루트 찾기와 flatviews_init

address_space_init 함수를 설명할 때, 마지막 두 개의 함수 호출까지 이야기했다.

void address_space_init(AddressSpace *as, MemoryRegion *root, const char *name)
{
    memory_region_ref(root);
    as->root = root;
    as->current_map = NULL;
    as->ioeventfd_nb = 0;
    as->ioeventfds = NULL;
    QTAILQ_INIT(&as->listeners);
    QTAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link);
    as->name = g_strdup(name ? name : "anonymous");
    address_space_update_topology(as);
    address_space_update_ioeventfds(as);
}

 

static void address_space_update_topology(AddressSpace *as)
{
    MemoryRegion *physmr = memory_region_get_flatview_root(as->root);

    flatviews_init();
    if (!g_hash_table_lookup(flat_views, physmr)) {
        generate_memory_topology(physmr);
    }
    address_space_set_flatview(as);
}

드디어 이 함수까지 돌아왔다. 

먼저 address_space_update_topology 함수의 매개변수 AddressSpace *as에 대응되는 실제 인자를 살펴보자.

void address_space_init(AddressSpace *as, MemoryRegion *root, const char *name)
{
    memory_region_ref(root);
    as->root = root;
    as->current_map = NULL;
    as->ioeventfd_nb = 0;
    as->ioeventfds = NULL;
    QTAILQ_INIT(&as->listeners);
    QTAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link);
    as->name = g_strdup(name ? name : "anonymous");
    address_space_update_topology(as);
    address_space_update_ioeventfds(as);
}

앞에서 여러 번 언급했듯이, 이 as가 실제로 가리키는 것은 바로 address_space_memory다.

static void memory_map_init(void)
{
    system_memory = g_malloc(sizeof(*system_memory));

    memory_region_init(system_memory, NULL, "system", UINT64_MAX);
    address_space_init(&address_space_memory, system_memory, "memory");

    system_io = g_malloc(sizeof(*system_io));
    memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io",
                          65536);
    address_space_init(&address_space_io, system_io, "I/O");
}

as->root는 address_space_init 함수 안에서 할당된 것이고, 실제로는 바로 system_memory다.

void address_space_init(AddressSpace *as, MemoryRegion *root, const char *name)
{
    memory_region_ref(root);
    as->root = root;
    as->current_map = NULL;
    as->ioeventfd_nb = 0;
    ...

 

memory_region_get_flatview_root 함수

그럼 첫 번째 함수 호출의 해석을 시작한다.

MemoryRegion *physmr = memory_region_get_flatview_root(as->root);

 

static MemoryRegion *memory_region_get_flatview_root(MemoryRegion *mr)
{
    while (mr->enabled) {
        if (mr->alias) {
            if (!mr->alias_offset && int128_ge(mr->size, mr->alias->size)) {
                /* The alias is included in its entirety.  Use it as
                 * the "real" root, so that we can share more FlatViews.
                 */
                mr = mr->alias;
                continue;
            }
        } else if (!mr->terminates) {
            unsigned int found = 0;
            MemoryRegion *child, *next = NULL;
            QTAILQ_FOREACH(child, &mr->subregions, subregions_link) {
                if (child->enabled) {
                    if (++found > 1) {
                        next = NULL;
                        break;
                    }
                    if (!child->addr && int128_ge(mr->size, child->size)) {
                        /* A child is included in its entirety.  If it's the only
                         * enabled one, use it in the hope of finding an alias down the
                         * way. This will also let us share FlatViews.
                         */
                        next = child;
                    }
                }
            }
            if (found == 0) {
                return NULL;
            }
            if (next) {
                mr = next;
                continue;
            }
        }

        return mr;
    }

    return NULL;
}

여기서 memory_region_get_flatview_root 함수의 매개변수 MemoryRegion *mr에 대응되는 실제 인자는 바로 system_memory다.

다시 말해 가상머신 메모리 주소 공간 address_space_memory에 대응되는 루트 MemoryRegion인 system_memory다.

그러면 memory_region_get_flatview_root 함수의 역할은 그 이름이 나타내는 것처럼, 가상머신 메모리 주소 공간 안의 루트가 flat 평탄화된 이후의 루트를 얻어내는 것이다.

 

flatviews_init 함수 

다음은 두 번째 함수인 flatviews_init이다.

static void address_space_update_topology(AddressSpace *as)
{
    MemoryRegion *physmr = memory_region_get_flatview_root(as->root);

    flatviews_init();
    if (!g_hash_table_lookup(flat_views, physmr)) {
        generate_memory_topology(physmr);
    }
    address_space_set_flatview(as);
}
static void flatviews_init(void)
{
    static FlatView *empty_view;

    if (flat_views) {
        return;
    }

    flat_views = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL,
                                       (GDestroyNotify) flatview_unref);
    if (!empty_view) {
        empty_view = generate_memory_topology(NULL);
        /* We keep it alive forever in the global variable.  */
        flatview_ref(empty_view);
    } else {
        g_hash_table_replace(flat_views, NULL, empty_view);
        flatview_ref(empty_view);
    }
}

flatviews_init 함수는 어떤 매개변수도 받지 않으니, 이전 함수들과는 아무 관련이 없다고 할 수 있다.

하지만 완전히 무관한 것은 아닌데, flat_views가 존재하기 때문이다. flat_views는 정적 전역변수이고, 같은 파일(softmmu/memory.c)에 정의되어 있는데, 아래와 같다.

static GHashTable *flat_views;

전역변수이니, 초기값은 반드시 NULL이다. 그러면 flatviews_init 함수에서는 계속 아래로 진행할 수 있고, 이미 생성된 적이 있어서 바로 리턴해 버리는 일은 없다.

 

이제 flatviews_init 함수의 이어지는 코드를 분석해보자.

현재까지 호출 흐름 정리

 

3. flatviews_init 과 GHashTable

지난번 address_space_init 함수 안에서 호출하는 address_space_update_topology 함수를 설명할 때,

flatviews_init 함수까지 이야기했는데 다 끝내지 못했다.

static void flatviews_init(void)
{
    static FlatView *empty_view;

    if (flat_views) {
        return;
    }

    flat_views = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL,
                                       (GDestroyNotify) flatview_unref);
    if (!empty_view) {
        empty_view = generate_memory_topology(NULL);
        /* We keep it alive forever in the global variable.  */
        flatview_ref(empty_view);
    } else {
        g_hash_table_replace(flat_views, NULL, empty_view);
        flatview_ref(empty_view);
    }
}

지난 번 이야기했듯이, flat_views는 정적 전역변수이고, 같은 파일(softmmu/memory.c)에 정의되어있다.

static GHashTable *flat_views;

만약 flat_views가 처음 생성되는 것이 아니라면(이전에 이미 생성된 적이 있다면), 바로 리턴한다. 여기서는 처음 생성되는 경우이기 때문에, 계속 아래로 진행한다.

 

그 다음으로 호출하는 것이 바로 g_hash_table_new_full 함수다.

flat_views = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL,
                                   (GDestroyNotify) flatview_unref);

이 부분의 내용은 glib 라이브러리 안의 해시 테이블 GHashTable과 관련된 내용이다.

 

hash 테이블은 key-value 형식으로 접근하는 데이터 구조를 제공하는데, 지정된 key 값을 통해 그와 관련된 value 값에 빠르게 접근할 수 있다. hash 테이블의 전형적인 사용법 중 하나가 바로 dictionary이다.

단어의 첫 글자를 통해 단어를 빠르게 찾을 수 있다. hash 테이블에 대한 자세한 설명은 데이터 구조 관련 책을 참고하면 되고, 여기서는 glib 라이브러리의 hash 테이블의 기본적인 사용법만 소개한다.

 

hash 테이블을 사용하려면 먼저 그것을 생성해야 한다. glib 라이브러리에는 hash 테이블을 생성하는 데 쓸 수 있는 함수가 두 개 있는데, 각각 g_hash_table_new()와 g_hash_table_new_full()이다. 이들의 원형은 아래와 같다.

GHashTable * g_hash_table_new(GHashFunc hash_func, GEqualFunc key_equal_func);

 

GHashTable * g_hash_table_new_full(GHashFunc hash_func,
                                   GEqualFunc key_equal_func,
                                   GDestroyNotify key_destroy_func,
                                   GDestroyNotify value_destroy_func);

그 중에서

  • GHashFunc hash_func —— hash_func는 하나의 함수인데, key를 위해 hash 값을 만들어 준다.
  • GEqualFunc key_equal_func —— key_equal_func는 두 key가 같은지 비교하는 데 사용된다.
  • GDestroyNotify key_destroy_func —— hash 테이블에서 어떤 항목을 삭제하거나 폐기할 때, glib 라이브러리가 자동으로 이 함수를 호출해서 key가 차지하던 메모리 공간을 해제한다. key가 동적으로 메모리를 할당하는 hash 테이블의 경우 매우 유용하다.
  • GDestroyNotify value_destroy_func —— value_destroy_func의 역할은 key_destroy_func와 비슷한데, 다만 해제하는 것이 value가 차지하던 메모리 공간이라는 점이 다르다.

 

GHashTable *g_hash_table_new(GHashFunc hash_func, GEqualFunc key_equal_func);

  • 기능: hash 테이블을 생성한다.
  • 매개변수:
    • hash_func —— hash 값을 만드는 함수다. key를 위해 하나의 hash 값을 만든다. 여기서는 glib 함수 라이브러리에 기본 내장된 g_str_hash를 사용할 수도 있고, 직접 만든 함수로 규칙을 세울 수도 있다.
    • key_equal_func —— key를 비교하는 데 쓰이는 함수인데, 테이블에 등록할 때 key의 비교가 이루어진다. 마찬가지로 여기서도 glib에 내장된 g_int_equal을 쓸 수도 있고, 직접 만든 함수로 비교 규칙을 세울 수도 있다.
  • 반환값: 핸들을 하나 리턴한다.

GHashTable* g_hash_table_new_full(GHashFunc hash_func, GEqualFunc key_equal_func, GDestroyNotify key_destroy_func, GDestroyNotify value_destroy_func);

  • 기능: 마찬가지로 hash 테이블을 만드는 데 쓴다.
    • g_hash_table_new보다 데이터 처리 함수가 두 개 더 있는데, g_hash_table_new의 업그레이드 버전에 해당한다.
    • 실제 프로젝트에서는 많은 경우 메모리 관리가 필요하기 때문에, 우리 예제에서 사용할 것은 바로 이 함수다. 이걸 쓸 줄 알면 위의 것도 쓸 수 있게 된다.
  • 매개변수:
    • hash_func —— hash 값을 만드는 함수인데, key를 위해 하나의 hash 값을 만든다. 여기서는 glib 함수 라이브러리에 기본 내장된 g_str_hash를 사용할 수도 있고, 직접 만든 함수로 규칙을 세울 수도 있다.
    • key_equal_func —— key를 비교하는 데 쓰는 함수인데, 이 함수는 해시 테이블에서 키 값을 찾을 때 사용된다. 마찬가지로 여기서도 glib에 내장된 g_int_equal을 쓸 수도 있고, 직접 만든 함수로 비교 규칙을 세울 수도 있다.
    • key_destroy_func —— key 데이터 처리 함수인데, 메모리를 해제하는 데 쓴다.
    • value_destroy_func —— value 데이터 처리 함수인데, 메모리를 해제하는 데 쓴다.
  • 반환값: 핸들을 하나 리턴한다.

코드로 돌아와서,

flat_views = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL,
                                   (GDestroyNotify) flatview_unref);

이 코드의 의미는 바로, g_direct_hash 함수를 사용해서 해시 테이블을 하나 생성하고, key를 비교하는 함수로 g_direct_equal()을 설정하고, key를 삭제하는 함수는 설정하지 않고, value를 삭제하는 함수로 flatview_unref()를 설정한다는 것이다. 함수가 리턴한 핸들은 flat_views에 보관한다.

GHashTable 구조 시각화

 

이제 flatviews_init 함수의 나머지 코드 분석을 해보자

 

4. generate_memory_topology

지금까지는 FlatRange의 정의, 그리고 FlatView와 FlatRange의 관계를 중점적으로 다뤘다.

이번에는 FlatView를 만드는 함수인 generate_memory_topology를 자세히 해석한다. 

다음은 generate_memory_topology 함수와 그것을 호출하는 address_space_update_topology 함수의 소스코드다.

static void address_space_update_topology(AddressSpace *as)
{
    MemoryRegion *physmr = memory_region_get_flatview_root(as->root);

    flatviews_init();
    if (!g_hash_table_lookup(flat_views, physmr)) {
        generate_memory_topology(physmr);
    }
    address_space_set_flatview(as);
}

 

/* Render a memory topology into a list of disjoint absolute ranges. */
static FlatView *generate_memory_topology(MemoryRegion *mr)
{
    int i;
    FlatView *view;

    view = flatview_new(mr);

    if (mr) {
        render_memory_region(view, mr, int128_zero(),
                             addrrange_make(int128_zero(), int128_2_64()),
                             false, false);
    }
    flatview_simplify(view);

    view->dispatch = address_space_dispatch_new(view);
    for (i = 0; i < view->nr; i++) {
        MemoryRegionSection mrs =
            section_from_flat_range(&view->ranges[i], view);
        flatview_add_to_dispatch(view, &mrs);
    }
    address_space_dispatch_compact(view->dispatch);
    g_hash_table_replace(flat_views, mr, view);

    return view;
}

 

먼저 generate_memory_topology 함수의 전체 기능을 정리해본다. generate_memory_topology 함수는 총 두 가지 기능을 가진다.

(1) 첫 번째 기능은 render_memory_region 함수를 호출해서 하나의 MemoryRegion을 펼치고, 펼친 결과를 첫 번째 FlatView에 기록하는 것이다. render_memory_region 함수는 자신의 모든 자식 Region들을 재귀적으로 호출해서, 잎(leaf) 노드에 도달할 때까지 펼친다.

(2) 두 번째 기능은 flatview_simplify 함수를 호출해서(simplify는 "단순화"라는 뜻이다) FlatView 안에서 병합 가능한 FlatRange들을 병합함으로써 FlatRange의 개수를 줄이는 것이다.

 

이어서 generate_memory_topology 함수의 구현 세부사항을 구체적으로 분석한다.

generate_memory_topology 함수는 먼저 flatview_new 함수를 호출해서 빈 FlatView를 만들고 초기화한다.

FlatView *view;
view = flatview_new(mr);

 

static FlatView *flatview_new(MemoryRegion *mr_root)
{
    FlatView *view;

    view = g_new0(FlatView, 1);
    view->ref = 1;
    view->root = mr_root;
    memory_region_ref(mr_root);
    trace_flatview_new(view, mr_root);

    return view;
}

 

void memory_region_ref(MemoryRegion *mr)
{
    /* MMIO callbacks most likely will access data that belongs
     * to the owner, hence the need to ref/unref the owner whenever
     * the memory region is in use.
     *
     * The memory region is a child of its owner.  As long as the
     * owner doesn't call unparent itself on the memory region,
     * ref-ing the owner will also keep the memory region alive.
     * Memory regions without an owner are supposed to never go away;
     * we do not ref/unref them because it slows down DMA sensibly.
     */
    if (mr && mr->owner) {
        object_ref(mr->owner);
    }
}

이어서, generate_memory_topology는 render_memory_region 함수를 호출해서 트리 형태의 MemoryRegion을 펼친다.

그 안에서 128비트 정수 연산이 좀 다뤄지는데, 여기서는 자세하게 파고들지 않고 일반적인 정수 연산이라고 생각하면 된다.

/* Render a memory region into the global view.  Ranges in @view obscure
 * ranges in @mr.
 */
static void render_memory_region(FlatView *view,
                                 MemoryRegion *mr,
                                 Int128 base,
                                 AddrRange clip,
                                 bool readonly,
                                 bool nonvolatile)
{
    MemoryRegion *subregion;
    unsigned i;
    hwaddr offset_in_region;
    Int128 remain;
    Int128 now;
    FlatRange fr;
    AddrRange tmp;

    if (!mr->enabled) {
        return;
    }

    int128_addto(&base, int128_make64(mr->addr));
    readonly |= mr->readonly;
    nonvolatile |= mr->nonvolatile;

    tmp = addrrange_make(base, mr->size);

    if (!addrrange_intersects(tmp, clip)) {
        return;
    }

    clip = addrrange_intersection(tmp, clip);

    if (mr->alias) {
        int128_subfrom(&base, int128_make64(mr->alias->addr));
        int128_subfrom(&base, int128_make64(mr->alias_offset));
        render_memory_region(view, mr->alias, base, clip,
                             readonly, nonvolatile);
        return;
    }

    /* Render subregions in priority order. */
    QTAILQ_FOREACH(subregion, &mr->subregions, subregions_link) {
        render_memory_region(view, subregion, base, clip,
                             readonly, nonvolatile);
    }

    if (!mr->terminates) {
        return;
    }

    offset_in_region = int128_get64(int128_sub(clip.start, base));
    base = clip.start;
    remain = clip.size;

    fr.mr = mr;
    fr.dirty_log_mask = memory_region_get_dirty_log_mask(mr);
    fr.romd_mode = mr->romd_mode;
    fr.readonly = readonly;
    fr.nonvolatile = nonvolatile;

    /* Render the region itself into any gaps left by the current view. */
    for (i = 0; i < view->nr && int128_nz(remain); ++i) {
        if (int128_ge(base, addrrange_end(view->ranges[i].addr))) {
            continue;
        }
        if (int128_lt(base, view->ranges[i].addr.start)) {
            now = int128_min(remain,
                             int128_sub(view->ranges[i].addr.start, base));
            fr.offset_in_region = offset_in_region;
            fr.addr = addrrange_make(base, now);
            flatview_insert(view, i, &fr);
            ++i;
            int128_addto(&base, now);
            offset_in_region += int128_get64(now);
            int128_subfrom(&remain, now);
        }
        now = int128_sub(int128_min(int128_add(base, remain),
                                    addrrange_end(view->ranges[i].addr)),
                         base);
        int128_addto(&base, now);
        offset_in_region += int128_get64(now);
        int128_subfrom(&remain, now);
    }
    if (int128_nz(remain)) {
        fr.offset_in_region = offset_in_region;
        fr.addr = addrrange_make(base, remain);
        flatview_insert(view, i, &fr);
    }
}

render_memory_region 함수는 QEMU 메모리 평면화의 핵심 함수다. 

 

5. render_memory_region 함수

/* Render a memory region into the global view.  Ranges in @view obscure
 * ranges in @mr.
 */
static void render_memory_region(FlatView *view,
                                 MemoryRegion *mr,
                                 Int128 base,
                                 AddrRange clip,
                                 bool readonly,
                                 bool nonvolatile)
{
    MemoryRegion *subregion;
    unsigned i;
    hwaddr offset_in_region;
    Int128 remain;
    Int128 now;
    FlatRange fr;
    AddrRange tmp;

    if (!mr->enabled) {
        return;
    }

    int128_addto(&base, int128_make64(mr->addr));
    readonly |= mr->readonly;
    nonvolatile |= mr->nonvolatile;

    tmp = addrrange_make(base, mr->size);

    if (!addrrange_intersects(tmp, clip)) {
        return;
    }

    clip = addrrange_intersection(tmp, clip);

    if (mr->alias) {
        int128_subfrom(&base, int128_make64(mr->alias->addr));
        int128_subfrom(&base, int128_make64(mr->alias_offset));
        render_memory_region(view, mr->alias, base, clip,
                             readonly, nonvolatile);
        return;
    }

    /* Render subregions in priority order. */
    QTAILQ_FOREACH(subregion, &mr->subregions, subregions_link) {
        render_memory_region(view, subregion, base, clip,
                             readonly, nonvolatile);
    }

    if (!mr->terminates) {
        return;
    }

    offset_in_region = int128_get64(int128_sub(clip.start, base));
    base = clip.start;
    remain = clip.size;

    fr.mr = mr;
    fr.dirty_log_mask = memory_region_get_dirty_log_mask(mr);
    fr.romd_mode = mr->romd_mode;
    fr.readonly = readonly;
    fr.nonvolatile = nonvolatile;

    /* Render the region itself into any gaps left by the current view. */
    for (i = 0; i < view->nr && int128_nz(remain); ++i) {
        if (int128_ge(base, addrrange_end(view->ranges[i].addr))) {
            continue;
        }
        if (int128_lt(base, view->ranges[i].addr.start)) {
            now = int128_min(remain,
                             int128_sub(view->ranges[i].addr.start, base));
            fr.offset_in_region = offset_in_region;
            fr.addr = addrrange_make(base, now);
            flatview_insert(view, i, &fr);
            ++i;
            int128_addto(&base, now);
            offset_in_region += int128_get64(now);
            int128_subfrom(&remain, now);
        }
        now = int128_sub(int128_min(int128_add(base, remain),
                                    addrrange_end(view->ranges[i].addr)),
                         base);
        int128_addto(&base, now);
        offset_in_region += int128_get64(now);
        int128_subfrom(&remain, now);
    }
    if (int128_nz(remain)) {
        fr.offset_in_region = offset_in_region;
        fr.addr = addrrange_make(base, remain);
        flatview_insert(view, i, &fr);
    }
}

 

render_memory_region 함수의 본질적인 역할은 하나의 MemoryRegion을 여러 개의 FlatRange로 변환한 다음, 첫 번째 인자인 FlatView의 FlatRange 멤버에 끼워 넣는 것이다.

 

전체 평면화 결과를 다시 한 번 그림으로 보면 아래와 같다.

 

/* Range of memory in the global map.  Addresses are absolute. */
struct FlatRange {
    MemoryRegion *mr;
    hwaddr offset_in_region;
    AddrRange addr;
    uint8_t dirty_log_mask;
    bool romd_mode;
    bool readonly;
    bool nonvolatile;
};

 

render_memory_region에는 6개의 인자가 있다.

static void render_memory_region(FlatView *view,
                                 MemoryRegion *mr,
                                 Int128 base,
                                 AddrRange clip,
                                 bool readonly,
                                 bool nonvolatile)

  1. FlatView *view
    • 첫 번째 인자는 전역(global) FlatView를 나타낸다. 즉 하나의 AddressSpace가 나타내는 FlatView다.
  2. MemoryRegion *mr
    • 두 번째 인자는 펼쳐야 할(전개할) MemoryRegion이다.
  3. Int128 base
    • 세 번째 인자는 base 값이고, 곧 펼쳐질 MemoryRegion의 시작 위치를 나타낸다.
  4. AddrRange clip
    • 네 번째 인자는 가상머신 물리 주소의 한 구간을 나타낸다.
  5. bool readonly
    • 다섯 번째 인자는 MemoryRegion이 읽기 전용인지 여부를 나타낸다.
  6. bool nonvolatile
    • 여섯 번째 인자는 대응하는 FlatRange가 비휘발성(non-volatile)인지 휘발성인지를 나타낸다.

 

render_memory_region 함수는 자기 자신을 재귀적으로 호출한다.

/* Render subregions in priority order. */
    QTAILQ_FOREACH(subregion, &mr->subregions, subregions_link) {
        render_memory_region(view, subregion, base, clip,
                             readonly, nonvolatile);
    }

 

재귀 동작의 흐름

매번 자식 region(subregion)을 두 번째 인자로 넘기고, 펼친 결과인 FlatRange를 첫 번째 인자 FlatView의 ranges 배열 멤버에 추가한다.

한 번의 render_memory_region 함수가 반환되면, 그건 곧 하나의 MemoryRegion이 펼쳐지기를 끝냈다는 뜻이다. 가장 최상위의 render_memory_region 함수가 반환되면, 전체 AddressSpace의 루트 MemoryRegion 전개가 모두 끝난 것이다.

 

이런 식으로 설명하면 너무 추상적이고 이해하기 어렵다. 이제 실제 예시를 들어 위의 메커니즘을 자세하게 해석한다.

 

6. render_memory_region flattening 심층

1. render_memory_region 함수 코드 전체

핵심이 되는 함수다.

/* Render a memory region into the global view.  Ranges in @view obscure
 * ranges in @mr.
 */
static void render_memory_region(FlatView *view,
                                 MemoryRegion *mr,
                                 Int128 base,
                                 AddrRange clip,
                                 bool readonly,
                                 bool nonvolatile)
{
    MemoryRegion *subregion;
    unsigned i;
    hwaddr offset_in_region;
    Int128 remain;
    Int128 now;
    FlatRange fr;
    AddrRange tmp;

    if (!mr->enabled) {
        return;
    }

    int128_addto(&base, int128_make64(mr->addr));
    readonly |= mr->readonly;
    nonvolatile |= mr->nonvolatile;

    tmp = addrrange_make(base, mr->size);

    if (!addrrange_intersects(tmp, clip)) {
        return;
    }

    clip = addrrange_intersection(tmp, clip);

    if (mr->alias) {
        int128_subfrom(&base, int128_make64(mr->alias->addr));
        int128_subfrom(&base, int128_make64(mr->alias_offset));
        render_memory_region(view, mr->alias, base, clip,
                             readonly, nonvolatile);
        return;
    }

    /* Render subregions in priority order. */
    QTAILQ_FOREACH(subregion, &mr->subregions, subregions_link) {
        render_memory_region(view, subregion, base, clip,
                             readonly, nonvolatile);
    }

    if (!mr->terminates) {
        return;
    }

    offset_in_region = int128_get64(int128_sub(clip.start, base));
    base = clip.start;
    remain = clip.size;

    fr.mr = mr;
    fr.dirty_log_mask = memory_region_get_dirty_log_mask(mr);
    fr.romd_mode = mr->romd_mode;
    fr.readonly = readonly;
    fr.nonvolatile = nonvolatile;

    /* Render the region itself into any gaps left by the current view. */
    for (i = 0; i < view->nr && int128_nz(remain); ++i) {
        if (int128_ge(base, addrrange_end(view->ranges[i].addr))) {
            continue;
        }
        if (int128_lt(base, view->ranges[i].addr.start)) {
            now = int128_min(remain,
                             int128_sub(view->ranges[i].addr.start, base));
            fr.offset_in_region = offset_in_region;
            fr.addr = addrrange_make(base, now);
            flatview_insert(view, i, &fr);
            ++i;
            int128_addto(&base, now);
            offset_in_region += int128_get64(now);
            int128_subfrom(&remain, now);
        }
        now = int128_min(int128_sub(int128_add(base, remain),
                         addrrange_end(view->ranges[i].addr)),
                         base);
        int128_addto(&base, now);
        offset_in_region += int128_get64(now);
        int128_subfrom(&remain, now);
    }
    if (int128_nz(remain)) {
        fr.offset_in_region = offset_in_region;
        fr.addr = addrrange_make(base, remain);
        flatview_insert(view, i, &fr);
    }
}

 

2. render_memory_region의 재귀 호출 구조

render_memory_region 함수는 자기 자신을 재귀 호출한다.

/* Render subregions in priority order. */
    QTAILQ_FOREACH(subregion, &mr->subregions, subregions_link) {
        render_memory_region(view, subregion, base, clip,
                             readonly, nonvolatile);
    }

매번 자식 region(subregion)을 두 번째 인자로 넘기고, 펼쳐진 FlatRange를 첫 번째 인자인 FlatView의 ranges 배열 멤버에 넣는다.

render_memory_region 함수가 한 번 리턴할 때마다 MemoryRegion 한 덩어리가 펼쳐졌다는 뜻이다.

최상위 render_memory_region 함수가 리턴할 때, 전체 AddressSpace의 루트 MemoryRegion이 모두 펼쳐진 것이다.

 

이렇게 말하면 좀 추상적이고 이해하기 어렵다. 여기서 실제 예시를 들어 코드를 이해해본다.

펼치려는 루트 MemoryRegion이 아래 그림의 MemoryRegion 1이고, 2~5는 그에 대응하는 자식 MemoryRegion이라고 가정한다

 

공간상으로 표현하면 다음과 같이 포함 관계가 그려진다.

먼저 MemoryRegion 1을 render_memory_region 함수의 두 번째 인자로 넘긴 다음, 재귀적으로 자기 자신을 호출해서 자식 Region 2~5를 펼친다.

/* Render subregions in priority order. */
    QTAILQ_FOREACH(subregion, &mr->subregions, subregions_link) {
        render_memory_region(view, subregion, base, clip,
                             readonly, nonvolatile);
    }

 

호출 관계는 다음 그림과 같다.

 

최하위의 4, 5가 펼쳐진 후에야 세 번째 render_memory_region 함수가 리턴할 수 있다. 마찬가지로 2, 3이 펼쳐진 후에야 첫 번째 render_memory_region 함수가 리턴할 수 있다.

 

3. 구체적인 예시로 풀어보기

여러 가지 상황을 고려해야 하다 보니 render_memory_region 함수는 꽤 복잡하다.

이 함수를 더 잘 이해하기 위해 예시를 통해 분석한다.

 

MemoryRegion mr1과 가상머신 주소 공간이 있다고 가정한다. mr1에는 자식 MemoryRegion인 mr2가 있고, 가상머신 물리 주소 공간에는 이미 두 개의 FlatRange가 펼쳐져 있는데, 각각 fr1과 fr2다. 아래 그림과 같다

 

mr1을 평탄화해야 한다고 가정하면, 먼저 mr1이 나타내는 주소와 clip의 교집합을 계산해야 한다.

여기서 clip의 초기값은 (0, UINT64_MAX)라고 가정하면, 새로 계산한 clip은 사실상 mr1이 대표하는 범위가 된다. 대응되는 코드 조각은 다음과 같다.

int128_addto(&base, int128_make64(mr->addr));
    readonly |= mr->readonly;
    nonvolatile |= mr->nonvolatile;

    tmp = addrrange_make(base, mr->size);

    if (!addrrange_intersects(tmp, clip)) {
        return;
    }

    clip = addrrange_intersection(tmp, clip);

MemoryRegion의 정의와 render_memory_region 함수의 인자 clip에 대한 설명을 대조해서 살펴본다.

 

여기서 clip이 어떻게 좁혀지는지는 MemoryRegion의 addr/size 필드를 함께 보면 이해하기 쉽다

if (mr->alias) {
        int128_subfrom(&base, int128_make64(mr->alias->addr));
        int128_subfrom(&base, int128_make64(mr->alias_offset));
        render_memory_region(view, mr->alias, base, clip,
                             readonly, nonvolatile);
        return;
    }

 

4. 자식 region(mr2)을 flattening하기

다음으로는 각 자식 region을 flattening한다.

/* Render subregions in priority order. */
    QTAILQ_FOREACH(subregion, &mr->subregions, subregions_link) {
        render_memory_region(view, subregion, base, clip,
                             readonly, nonvolatile);
    }

또 이 코드 조각으로 돌아왔다. 다만 주의할 점은, 이때 render_memory_region 함수를 호출할 때 clip은 이미 mr2의 범위에 속한다는 것이다.

이어서 offset_in_region을 계산한다. 값은 clip.start - base다. 코드 조각은 다음과 같다.

offset_in_region = int128_get64(int128_sub(clip.start, base));
    base = clip.start;
    remain = clip.size;

주의할 점은, 이때 base는 mr2->addr (위 그림 참고)이라서, 이 offset_in_region은 곧 생성될 FlatRange의 mr2 기준 시작 위치가 되고, 처음에는 0이다.

이어서 base를 clip의 시작 위치로 설정하고, 펼쳐야 할 크기 remain은 clip.size로 설정한다. 즉 mr2->size다.

다음으로는 FlatRange 하나를 초기화한다.

fr.mr = mr;
    fr.dirty_log_mask = memory_region_get_dirty_log_mask(mr);
    fr.romd_mode = mr->romd_mode;
    fr.readonly = readonly;
    fr.nonvolatile = nonvolatile;

이때부터 mr2를 펼칠 준비를 시작한다. mr2는 이미 일부가 펼쳐져 있다고 가정한다 (이건 가능하다. 한 메모리 구간은 여러 MemoryRegion으로 기술될 수 있기 때문이다).

 

이때 view의 모든 FlatRange를 순회해야 한다. 만약 base의 값이 FlatRange의 끝 경계보다 크거나 같다면, 즉 range가 mr2의 왼쪽에 위치한다면, 처리할 필요 없다.

/* Render the region itself into any gaps left by the current view. */
    for (i = 0; i < view->nr && int128_nz(remain); ++i) {
        if (int128_ge(base, addrrange_end(view->ranges[i].addr))) {
            continue;
        }
        }
        ......
    }

 

만약 base의 값이 range의 시작 주소보다 작다면, 즉 위 그림에서 fr2 왼쪽 진한 파란색 부분의 경우에는, 이 진한 파란색 구역을 펼쳐야 한다.

/* Render the region itself into any gaps left by the current view. */
    for (i = 0; i < view->nr && int128_nz(remain); ++i) {
        ......
        if (int128_lt(base, view->ranges[i].addr.start)) {
            now = int128_min(remain,
                             int128_sub(view->ranges[i].addr.start, base));
            fr.offset_in_region = offset_in_region;
            fr.addr = addrrange_make(base, now);
            flatview_insert(view, i, &fr);
            ++i;
            int128_addto(&base, now);
            offset_in_region += int128_get64(now);
            int128_subfrom(&remain, now);
        }
        ......
    }

 

단계별 분석

 

1) now를 계산한다. 즉 왼쪽 진한 색 영역의 길이다.

now = int128_min(remain, int128_sub(view->ranges[i].addr.start, base));

 

2) 그 다음 fr.offset_in_region을 mr2 안에서의 오프셋으로 설정한다 (이 오프셋은 0이다).

offset_in_region = int128_get64(int128_sub(clip.start, base));
    ......
    /* Render the region itself into any gaps left by the current view. */
    for (i = 0; i < view->nr && int128_nz(remain); ++i) {
        ......
            fr.offset_in_region = offset_in_region;
        ......
    }

 

3) fr.addr은 이 FlatRange가 가상머신 물리 주소 공간에서 차지하는 위치, 즉 왼쪽 진한 색 영역의 시작 위치를 나타낸다.

fr.addr = addrrange_make(base, now);

 

4) 이어서 flatview_insert 함수를 호출해서 이 fr을 view에 삽입한다.

flatview_insert(view, i, &fr);

 

5) 그런 다음 base를 갱신한다. 이때 base는 fr2의 시작 위치와 같아진다.

int128_addto(&base, now);

 

6) 이어서 remain을 갱신한다. 

offset_in_region += int128_get64(now);
int128_subfrom(&remain, now);

 

5. for 루프 안의 다음 코드 조각 - fr2 건너뛰기

/* Render the region itself into any gaps left by the current view. */
    for (i = 0; i < view->nr && int128_nz(remain); ++i) {
        ......
        now = int128_sub(int128_min(int128_add(base, remain),
                         addrrange_end(view->ranges[i].addr)),
                         base);
        int128_addto(&base, now);
        offset_in_region += int128_get64(now);
        int128_subfrom(&remain, now);
    }

이 코드 조각은 fr2를 건너뛰는 데 쓰인다. 물론 다른 상황도 있을 수 있다. 예를 들면 mr2의 꼬리가 fr2 안에 떨어지는 경우다. 그래서 여기서는 int128_min 함수를 통해 판단한다.

now = int128_sub(int128_min(int128_add(base, remain),
                             addrrange_end(view->ranges[i].addr)),
                             base);

이 시점에 첫 번째 진한 색 영역은 삽입이 완료된 상태다. 그 base는 fr2의 끝 주소이고, offset_in_region은 fr2의 길이와 왼쪽 진한 색 부분의 합이며, remain은 오른쪽 진한 색 부분의 길이다.

 

다음은 그 다음 루프 차례다. 그 다음 루프에서 오른쪽 진한 색 영역으로 구성된 FlatRange를 view에 삽입한다.

이런 식으로 유추하면, 최종적으로 render_memory_region 함수가 리턴할 때 view에는 mr1을 루트로 하는 FlatRange들이 가득 채워져 있게 된다.

 

6. render_memory_region 함수의 전체 기능 정리

여기까지 render_memory_region 함수 해석이 끝났다. 전체 기능을 한번 돌아보자:

render_memory_region 함수의 본질적인 작용은 하나의 MemoryRegion을 여러 개의 FlatRange로 변환한 다음, 첫 번째 인자인 FlatView의 FlatRange 멤버에 삽입하는 것이다.

 

 

7. 마무리: flatview_simplify / can_merge 재정리

generate_memory_topology 로 돌아가기 — simplify 적용 위치

render_memory_region 함수가 리턴된 후, generate_memory_topology 함수로 돌아간다 (softmmu/memory.c)

/* Render a memory topology into a list of disjoint absolute ranges. */
static FlatView *generate_memory_topology(MemoryRegion *mr)
{
    int i;
    FlatView *view;

    view = flatview_new(mr);

    if (mr) {
        render_memory_region(view, mr, int128_zero(),
                             addrrange_make(int128_zero(), int128_2_64()),
                             false, false);
    }
    flatview_simplify(view);

    view->dispatch = address_space_dispatch_new(view);
    for (i = 0; i < view->nr; i++) {
        MemoryRegionSection mrs =
            section_from_flat_range(&view->ranges[i], view);
        flatview_add_to_dispatch(view, &mrs);
    }
    address_space_dispatch_compact(view->dispatch);
    g_hash_table_replace(flat_views, mr, view);

    return view;
}

 

flatview_simplify 함수

generate_memory_topology 함수는 이어서 flatview_simplify 함수를 호출한다. 

/* Render a memory topology into a list of disjoint absolute ranges. */
static FlatView *generate_memory_topology(MemoryRegion *mr)
{
    int i;
    FlatView *view;

    view = flatview_new(mr);

    if (mr) {
        render_memory_region(view, mr, int128_zero(),
                             addrrange_make(int128_zero(), int128_2_64()),
                             false, false);
    }
    flatview_simplify(view);

    view->dispatch = address_space_dispatch_new(view);
    for (i = 0; i < view->nr; i++) {
        MemoryRegionSection mrs =
            section_from_flat_range(&view->ranges[i], view);
        flatview_add_to_dispatch(view, &mrs);
    }
    address_space_dispatch_compact(view->dispatch);
    g_hash_table_replace(flat_views, mr, view);

    return view;
}

 

flatview_simplify 함수

generate_memory_topology 함수는 이어서 flatview_simplify 함수를 호출한다.

/* Attempt to simplify a view by merging adjacent ranges */
static void flatview_simplify(FlatView *view)
{
    unsigned i, j, k;

    i = 0;
    while (i < view->nr) {
        j = i + 1;
        while (j < view->nr
               && can_merge(&view->ranges[j-1], &view->ranges[j])) {
            int128_addto(&view->ranges[i].addr.size, view->ranges[j].addr.size);
            ++j;
        }
        ++i;
        for (k = i; k < j; k++) {
            memory_region_unref(view->ranges[k].mr);
        }
        memmove(&view->ranges[i], &view->ranges[j],
                (view->nr - j) * sizeof(view->ranges[j]));
        view->nr -= j - i;
    }
}

 

generate_memory_topology 함수 안의 can_merge 함수는 같은 파일 안에 있고 (바로 위에 있음), 코드는 다음과 같다.

static bool can_merge(FlatRange *r1, FlatRange *r2)
{
    return int128_eq(addrrange_end(r1->addr), r2->addr.start)
        && r1->mr == r2->mr
        && int128_eq(int128_add(int128_make64(r1->offset_in_region),
                                r1->addr.size),
                     int128_make64(r2->offset_in_region))
        && r1->dirty_log_mask == r2->dirty_log_mask
        && r1->romd_mode == r2->romd_mode
        && r1->readonly == r2->readonly
        && r1->nonvolatile == r2->nonvolatile;
}

두 FlatRange를 합칠 수 있는지 판단하는 데 쓰인다. 만약 두 FlatRange가 인접해 있고 각종 속성이 전부 같다면, 이 두 FlatRange는 합칠 수 있고, 합쳐서 하나로 만든다.

 

address_space_update_topology 함수

이제 다시 generate_memory_topology 함수를 호출한 address_space_update_topology 함수로 돌아간다 (역시 softmmu/memory.c에 있음)

static void address_space_update_topology(AddressSpace *as)
{
    MemoryRegion *physmr = memory_region_get_flatview_root(as->root);

    flatviews_init();
    if (!g_hash_table_lookup(flat_views, physmr)) {
        generate_memory_topology(physmr);
    }
    address_space_set_flatview(as);
}

가상머신이 메모리 커밋을 할 때마다 address_space_update_topology 함수 안에서 generate_memory_topology 함수를 호출해서, 현재 가상머신 메모리의 low flattening 표현을 생성한다.

 

flattening 전체 호출 흐름 요약

 

5) listener notify (address_space_set_flatview)

qemu_init()
    ---> qemu_create_machine()
        ---> cpu_exec_init_all()
            ---> memory_map_init()
                ---> address_space_init()
                    ---> address_space_update_topology()

지금까지 address_space_update_topology 함수까지 이야기했고, 그에 대한 해석도 했다.

static void address_space_update_topology(AddressSpace *as)
{
    MemoryRegion *physmr = memory_region_get_flatview_root(as->root);

    flatviews_init();
    if (!g_hash_table_lookup(flat_views, physmr)) {
        generate_memory_topology(physmr);
    }
    address_space_set_flatview(as);
}

 

이제 이 안의 address_space_set_flatview 함수를 해석한다.

static void address_space_set_flatview(AddressSpace *as)
{
    FlatView *old_view = address_space_to_flatview(as);
    MemoryRegion *physmr = memory_region_get_flatview_root(as->root);
    FlatView *new_view = g_hash_table_lookup(flat_views, physmr);

    assert(new_view);

    if (old_view == new_view) {
        return;
    }

    if (old_view) {
        flatview_ref(old_view);
    }

    flatview_ref(new_view);

    if (!QTAILQ_EMPTY(&as->listeners)) {
        FlatView tmpview = { .nr = 0 }, *old_view2 = old_view;

        if (!old_view2) {
            old_view2 = &tmpview;
        }
        address_space_update_topology_pass(as, old_view2, new_view, false);
        address_space_update_topology_pass(as, old_view2, new_view, true);
    }

    /* Writes are protected by the BQL.  */
    qatomic_rcu_set(&as->current_map, new_view);
    if (old_view) {
        flatview_unref(old_view);
    }

    /* Note that all the old MemoryRegions are still alive up to this
     * point.  This relieves most MemoryListeners from the need to
     * ref/unref the MemoryRegions they get---unless they use them
     * outside the iothread mutex, in which case precise reference
     * counting is necessary.
     */
    if (old_view) {
        flatview_unref(old_view);
    }
}

 

address_space_set_flatview 함수는 시작하자마자, 먼저 옛 flatview와 새 flatview를 비교한다. 만약 동일하다면 바로 리턴한다.

FlatView *old_view = address_space_to_flatview(as);
MemoryRegion *physmr = memory_region_get_flatview_root(as->root);
FlatView *new_view = g_hash_table_lookup(flat_views, physmr);

assert(new_view);

if (old_view == new_view) {
    return;
}

 

앞 글에서 얘기했듯이, 지금은 메모리 초기화 단계이기 때문에 address_space_update_topology 함수 안에서 전역 flat_views에서는 분명히 찾을 수가 없다.

그래서 generate_memory_topology 함수가 호출돼 처음으로 FlatView를 생성하게 되고, 그 후에 address_space_set_flatview 함수가 호출되는 것이다.

static void address_space_update_topology(AddressSpace *as)
{
    MemoryRegion *physmr = memory_region_get_flatview_root(as->root);

    flatviews_init();
    if (!g_hash_table_lookup(flat_views, physmr)) {
        generate_memory_topology(physmr);
    }
    address_space_set_flatview(as);
}

따라서 옛 flatview와 새 flatview는 분명히 같지 않다.

FlatView *old_view = address_space_to_flatview(as);
MemoryRegion *physmr = memory_region_get_flatview_root(as->root);
FlatView *new_view = g_hash_table_lookup(flat_views, physmr);

assert(new_view);

if (old_view == new_view) {
    return;
}

 

만약 옛 flatview와 새 flatview가 다르다면, 메모리 구조에 변화가 생겼다는 뜻이다.

그러면 address_space_update_topology_pass 함수가 호출돼서 모든 listener에게 통지한다.

static void address_space_update_topology_pass(AddressSpace *as,
                                               const FlatView *old_view,
                                               const FlatView *new_view,
                                               bool adding)
{
    unsigned iold, inew;
    FlatRange *frold, *frnew;

    /* Generate a symmetric difference of the old and new memory maps.
     * Kill ranges in the old map, and instantiate ranges in the new map.
     */
    iold = inew = 0;
    while (iold < old_view->nr || inew < new_view->nr) {
        if (iold < old_view->nr) {
            frold = &old_view->ranges[iold];
        } else {
            frold = NULL;
        }
        if (inew < new_view->nr) {
            frnew = &new_view->ranges[inew];
        } else {
            frnew = NULL;
        }

        if (frold
            && (!frnew
                || int128_lt(frold->addr.start, frnew->addr.start)
                || (int128_eq(frold->addr.start, frnew->addr.start)
                    && !flatrange_equal(frold, frnew)))) {
            /* In old but not in new, or in both but attributes changed. */

            if (!adding) {
                flat_range_coalesced_io_del(frold, as);
                MEMORY_LISTENER_UPDATE_REGION(frold, as, Reverse, region_del);
            }

            ++iold;
        } else if (frold && frnew && flatrange_equal(frold, frnew)) {
            /* In both and unchanged (except logging may have changed) */

            if (adding) {
                MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, region_nop);
                if (frnew->dirty_log_mask & ~frold->dirty_log_mask) {
                    MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, log_start,
                                                  frold->dirty_log_mask,
                                                  frnew->dirty_log_mask);
                }
                if (frold->dirty_log_mask & ~frnew->dirty_log_mask) {
                    MEMORY_LISTENER_UPDATE_REGION(frnew, as, Reverse, log_stop,
                                                  frold->dirty_log_mask,
                                                  frnew->dirty_log_mask);
                }
            }

            ++iold;
            ++inew;
        } else {
            /* In new */

            if (adding) {
                MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, region_add);
                flat_range_coalesced_io_add(frnew, as);
            }

            ++inew;
        }
    }
}

 

address_space_update_topology_pass는 옛 FlatView와 새 FlatView의 "대칭 차집합(symmetric difference)"을 계산해서 처리한다. 세 가지 경우로 나뉜다.

그 안에서 MEMORY_LISTENER_UPDATE_REGION을 통해서 MEMORY_LISTENER_CALL을 호출하고, 모든 listener에게 통지한다.

 

MEMORY_LISTENER_UPDATE_REGION 매크로

/* No need to ref/unref .mr, the FlatRange keeps it alive.  */
#define MEMORY_LISTENER_UPDATE_REGION(fr, as, dir, callback, _args...)  \
    do {                                                                \
        MemoryRegionSection mrs = section_from_flat_range(fr,           \
                address_space_to_flatview(as));                         \
        MEMORY_LISTENER_CALL(as, callback, dir, &mrs, ##_args);         \
    } while(0)

 

 

전체 시퀀스 다이어그램

 

핵심 자료구조 관계도

 

 

전체 자료구조 관계 종합도

 

'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 코드 분석 #2 KVM CPU가상화  (0) 2024.07.08
QEMU 코드 분석 #1 파라미터 해석 · 모듈 초기화 · QOM · 속성  (0) 2024.03.25
    'System Programming/Hypervisor' 카테고리의 다른 글
    • QEMU 코드 분석 #5 PCI 디바이스 에뮬레이션 & 인터럽트
    • QEMU 코드 분석 #4 Machine & i440fx
    • QEMU 코드 분석 #2 KVM CPU가상화
    • QEMU 코드 분석 #1 파라미터 해석 · 모듈 초기화 · QOM · 속성
    1000sj
    1000sj

    티스토리툴바