1. 程式人生 > >qemu內存管理

qemu內存管理

sub for oca api linux nio 不同的應用 指定 內存結構

1 Qemu內存分布
技術分享圖片
2 內存初始化
Qemu中的內存模型,簡單來說就是Qemu申請用戶態內存並進行管理,並將該部分申請的內存註冊到對應的加速器(如KVM)中。這樣的模型有如下好處:

  1. 策略與機制分離。加速的機制由KVM負責,而如何調用加速的機制由Qemu負責
  2. 可以由Qemu設置多種內存模型,如UMA、NUMA等等
  3. 方便Qemu對特殊內存的管理(如MMIO)
  4. 內存的分配、回收、換出等都可以采用Linux原有的機制,不需要為KVM單獨開發。
  5. 兼容其他加速器模型(或者無加速器,單純使用Qemu做模擬)
    Qemu需要做的有兩方面工作:向KVM註冊用戶態內存空間,申請用戶態內存空間。
    Qemu主要通過如下結構來維護內存:

    / A system address space - I/O, memory, etc. /
    struct AddressSpace {
    char name;
    MemoryRegion
    root;
    FlatView current_map;
    int ioeventfd_nb;
    MemoryRegionIoeventfd ioeventfds;
    struct AddressSpaceDispatch
    dispatch;
    struct AddressSpaceDispatch next_dispatch;
    MemoryListener dispatch_listener;
    QTAILQ_ENTRY(AddressSpace) address_spaces_link;
    };
    "memory"的root是static MemoryRegion
    system_memory;
    使用鏈表address_spaces保存虛擬機的內存,該鏈表保存AddressSpace address_space_io和AddressSpace address_space_memory等信息
    void address_space_init(AddressSpace as, MemoryRegion root, const char *name)
    {
    if (QTAILQ_EMPTY(&address_spaces)) {
    memory_init();
    }

    memory_region_transaction_begin();

    as->root = root;
    as->current_map = g_new(FlatView, 1);
    flatview_init(as->current_map);
    as->ioeventfd_nb = 0;
    as->ioeventfds = NULL;
    QTAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link);
    as->name = g_strdup(name ? name : "anonymous");
    address_space_init_dispatch(as);
    memory_region_update_pending |= root->enabled;
    memory_region_transaction_commit();
    }
    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_listener_register(&core_memory_listener, &address_space_memory);
    }
    AddressSpace設置了一段內存,其主要信息存儲在root成員 中,root成員是個MemoryRegion結構,主要存儲內存區的結構。在Qemu中最主要的兩個AddressSpace是 address_space_memory和address_space_io,分別對應的MemoryRegion變量是system_memory和 system_io。
    Qemu的主函數是vl.c中的main函數,其中調用了configure_accelerator(),是KVM初始化的配置部分。
    configure_accelerator中首先根據命令行輸入的參數找到對應的accelerator,這裏是KVM。之後調用accel_list[i].init(),即kvm_init()。
    在kvm_init()函數中主要做如下幾件事情:

  6. s->fd = qemu_open("/dev/kvm", O_RDWR),打開kvm控制的總設備文件/dev/kvm
  7. s->vmfd = kvm_ioctl(s, KVM_CREATE_VM, 0),調用創建虛擬機的API
  8. kvm_check_extension,檢查各種extension,並設置對應的features
  9. ret = kvm_arch_init(s),做一些體系結構相關的初始化,如msr、identity map、mmu pages number等等
  10. kvm_irqchip_create,調用kvm_vm_ioctl(s, KVM_CREATE_IRQCHIP)在KVM中虛擬IRQ芯片
  11. memory_listener_register,該函數是初始化內存的主要函數
    memory_listener_register調用了兩次,分別註冊了 kvm_memory_listener和kvm_io_listener,即通用的內存和MMIO是分開管理的。以通用的內存註冊為例,函數首先在全局 的memory_listener鏈表中添加了kvm_memory_listener,之後調用listener_add_address_space 分別將該listener添加到address_space_memory和address_space_io中, address_space_io是虛機的io地址空間(設備的io port就分布在這個地址空間裏)。
    然後調用listener的region_add(即 kvm_region_add()),該函數最終調用了kvm_set_user_memory_region(),其中調用 kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem),該調用是最終將內存區域註冊到kvm中的函數。
    之後在vl.c的main函數中調用了cpu_exec_init_all() => memory_map_init(),設置system_memory和system_io。
    至此初始化好了所有Qemu中需要維護的相關的內存結構,並完成了在KVM中的註冊。下面需要初始化KVM中的MMU支持。
    ram_size內存大小從內存被讀取到ram_size中,在vl.c的main中調用machine->init()來初始化,machine是命令行指定的機器類型,默認的init是pc_init_pci
    pc_memory_init
    memory_region_allocate_system_memory
    memory_region_add_subregion
    memory_region_add_subregion_common
    memory_region_update_container_subregions
    memory_region_transaction_commit
    address_space_update_topology
    generate_memory_topology
    render_memory_region
    flatview_insert

3 內存分配
內存的分配實現函數為 ram_addr_t qemu_ram_alloc(ram_addr_t size, MemoryRegion *mr),輸出為該次分配的內存在所有分配內存中的順序偏移(即下圖中的紅色數字).
該函數最終調用phys_mem_alloc分配內存, 並將所分配的全部內存塊, 串在一個ram_blocks開頭的鏈表中, 如下示意:
技術分享圖片
上圖中分配了4個內存塊, 每次分配時偏移offset順序累加, host指向該內存塊在主機中的虛擬地址.
調用memory_listener_register註冊

4 內存映射
使用的相關結構體如下:
/ 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;
};
/
Flattened global view of current active memory hierarchy. Kept in sorted order./
struct FlatView {
unsigned ref;
FlatRange
ranges;
unsigned nr;
unsigned nr_allocated;
};
映射是將上面分配的地址塊映射為客戶機的物理地址, 函數如下, 輸入為映射後的物理地址, 內存偏移,通用內存塊的地址
static void memory_region_add_subregion_common(MemoryRegion mr, hwaddr offset, MemoryRegion subregion)
MemoryRegion mr:對應的是system_memory或者system_io,通過memory_listener_register函數註冊內存塊。
通用棧如下:
memory_region_update_container_subregions
memory_region_transaction_commit
address_space_update_topology
generate_memory_topology
address_space_update_topology_pass
memory_region_update_container_subregions函數在鏈表中尋找合適的位置插入,
/
插入指定的位置/
QTAILQ_FOREACH(other, &mr->subregions, subregions_link) {
if (subregion->priority >= other->priority) {
QTAILQ_INSERT_BEFORE(other, subregion, subregions_link);
goto done;
}
}
QTAILQ_INSERT_TAIL(&mr->subregions, subregion, subregions_link);
memory_region_transaction_commit中引入了新的結構address_spaces(AS),內存有不同的應用類型,address_spaces以鏈表形式存在,commit函數則是對所有AS執行 address_space_update_topology,先看AS在哪裏註冊的,就是前面提到的kvm_init裏面,執行 memory_listener_register,註冊了address_space_memory和address_space_io兩個,涉及的另 外一個結構體則是MemoryListener,有kvm_memory_listener和kvm_io_listener,就是用於監控內存映射關系 發生變化之後執行回調函數。
address_space_update_topology_pass函數比較之前的內存塊,做相應的處理
MEMORY_LISTENER_UPDATE_REGION函數,將變化的FlatRange構造一個MemoryRegionSection,然後 遍歷所有的memory_listeners,如果memory_listeners監控的內存區域和MemoryRegionSection一樣,則執 行第四個入參函數,如region_del函數,即kvm_region_del函數,這個是在kvm_init中初始化的。 kvm_region_add主要是kvm_set_phys_mem函數,主要是將MemoryRegionSection有效值轉換成KVMSlot 形式,在kvm_set_user_memory_region中使用kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem)傳遞給kernel。
5 客戶機物理地址到主機虛擬地址的轉換
5.1 地址屬性
內存映射是以頁為單位的, 也就意味著phys_offset的低12bit為0, Qemu使用這些bit標識地址屬性:
Bit 11-3 Bit 2 Bit 1 Bit 0
MMIO索引, 其中4個固定分配 SUBWIDTH SUBPAGE ROMD
0: RAM
1: ROM
2: UNASSIGNED
3: NOTDIRTY
5.2 客戶機物理地址到主機虛擬地址的轉換步驟
虛擬機因mmio退出時,qemu處理該退出事件,相關的函數:
void cpu_physical_memory_rw(hwaddr addr, uint8_t
buf, int len, int is_write)
該函數實現虛擬機的物理地址到主機虛擬地址的轉換

  1. 查找該地址所應的MemoryRegionSection結構, 函數為 static MemoryRegionSection phys_page_find(PhysPageEntry lp, hwaddr addr, Node nodes, MemoryRegionSection *sections), 即將客戶物理地址分為4段, 取每一段的索引查找下一段, 直至找到Level 3的MemoryRegionSection結構.
  2. 調函數void qemu_get_ram_ptr(ram_addr_t addr), 取主機虛擬地址起始位置, 再加上頁內偏移, 即為對應的主機虛擬地址
    6 Kvm映射
    static void kvm_set_phys_mem(MemoryRegionSection
    section, bool add)
    該函數把guest機的物理內存映射主機的虛擬內存
    typedef struct KVMSlot
    {
    hwaddr start_addr; /guest物理地址/
    ram_addr_t memory_size; /內存大小/
    void ram; /對應的虛擬地址/
    int slot; /
    對應的插槽號*/
    int flags;
    } KVMSlot;
    Qemu支持kvm時, 還需通知kvm將客戶機物理內存進行映射, 方法為先定義一個映射結構:
    struct kvm_userspace_memory_region memory = {
    .memory_size = len,
    .guest_phys_addr = phys_start, // 客戶機物理地址
    .userspace_addr = userspace_addr, // 主機虛擬地址, 而非上面的偏移
    .flags = log ? KVM_MEM_LOG_DIRTY_PAGES : 0,
    };
    然後調用kvm的ioctl r = kvm_vm_ioctl(kvm_state, KVM_SET_USER_MEMORY_REGION, &memory);同時, qemu的kvm用戶空間代碼, 還定義了一些結構如mapping/slot, 用於地址空間的管理, 如防止重復映射等.

qemu內存管理