本文涉及到的源碼是FreeBSD5.0Release,參考4.4BSD設計與實現相關章節,Matt Dillon的文章。
VM系統涉及的主要數據結構描述 1. vmspace 該結構用於描述一個進程的虛擬地址空間,其包含了平台無關性的vm_map結構和平台相關性的pmap結構,以及該進程內存使用的一些統計計量。 2. vm_map 該結構是描述與平台無關性的虛擬地址空間的最高層數據結構,其包含了一系列虛擬地址有效地址映射實體和這些映射的屬性。 3. vm_map_entry 該結構描述了一段虛擬地址空間(start – end),以及該段地址空間代表的是一種VM對象、另一個地址映射還是一個地址子映射,及其相應的共享保護和繼承等屬性。 4. vm_object 該結構描述了一段虛擬地址空間的數據來源,它可以描述一個文件、一段為零的內存和一個設備等等。 5. vm_page 該結構描述了一頁物理內存,是VM用於表述物理內存的低層數據結構。頁尺寸是在系統啟動時,由平台決定的。 6. pagerops (vm_pager) 該結構描述了VM對象的後台存儲如何訪問,在FreeBSD中,是通過pagerops結構描述函數指針,實現不同類型的對象的具體操作,在vm_object結構中,通過handle成員指定具體類型對象對應的數據結構,比如設備類型對應dev_t (cdev結構指針)。在一般OS描述中,采用vm_pager描述該目的的數據結構。 本文集中討論FreeBSD內核虛擬地址空間的管理,涉及到內核地址空間分配和內核地址空間動態分配。FreeBSD的內核空間總是被映射到每一個進程的地址空間的最高部分。和任何其它進程一樣,內核也是通過包含一系列的vm_map_entry結構實體的vm_map結構來管理一段地址空間的使用。子映射是內核映射特有的,用於隔離、限制一段地址空間以提供給內核子系統使用,比如mbuf操作。本文主要討論與平台無關性的內容,涉及到平台相關性時,以i386為例簡要說明。
1. SI_SUB_VM初始化 在系統啟動時,mi_startup()函數會調用SI_SUB_VM初始化與平台無關的VM系統,其定義是:“SYSINIT(vm_mem, SI_SUB_VM, SI_ORDER_FIRST, vm_mem_init, NULL)”。在vm_mem_init函數初始化之後,我們就只使用虛擬內存了,現在分析該函數的實現: vm_set_page_size(); 該函數設置頁面尺寸,i386是PAGE_SIZE(4K),記錄在系統統計vmmeter結構類型的全局變量cnt的v_page_size成員中。 virtual_avail = vm_page_startup(avail_start, avail_end, virtual_avail); 該語句初始化常駐內存模塊。分析函數vm_page_startup的參數和返回值:avail_start的值是從系統啟動時,匯編語言調用init386的入參first,指明有效內存的起始地址;avail_end是在getmemsize()函數結束時,通過avail_end = phys_avail[pa_indx];語句獲得,該函數是一個與平台相關的函數,這裡不作詳細討論,只須明白getmemsize()函數是獲得具體物理內存的尺寸;virtual_avail是指向第一個可用頁面的虛擬地址,在調用vm_page_startup函數前,是在pmap_bootstrap函數中獲得初始值,並在vm_page_startup函數中調整獲得真實的值。函數vm_page_startup將物理內存整理、分配為頁面單元,並初始化頁面管理模塊所需信息,每一個頁面單元被放置在自由鏈表中,該函數實現的詳細討論在頁調度中討論,作為內核管理涉及到的區域分配器初始化的一部分是在該函數中通過調用uma_startup函數實現的,該函數的實現在隨後討論。 vm_object_init(); 初始化VM的對象模塊,FreeBSD是通過統一的vm_object結構使用虛擬內存,該函數完成虛擬內存對象模塊所需信息的初始化。 vm_map_startup(); 初始化VM地址映射模塊。 kmem_init(virtual_avail, virtual_end); 該函數創建內核虛擬地址映射關系,將內核文本、數據、BSS和所有系統啟動時已經分配了的空間做一個映射,插入VM_MIN_KERNEL_ADDRESS和virtual_avail之間,余下的virtual_avail和virtual_end之間的地址空間是可用的自由空間。 pmap_init(avail_start, avail_end); 該函數初始化物理內存地址空間的映射關系。 vm_pager_init(); 該函數實現系統所支持的所有頁面接口類型的初始化,頁面接口為數據在其支持的存儲空間和物理內存之間的移動提供了一種機制,比如磁盤設備與內存之間,文件系統與內存之間。 至此,vm_mem_init函數執行完成,VM系統初始化完成。
2. 內核地址空間分配 VM系統內核使用的虛擬地址空間段提供了一套用於分配和釋放的函數,這些空間段可以從內核地址映射和子映射中分配獲得。 根據申請的頁是否可以被pageout守護進程調度,內核內存分配有兩種路徑。在VM子系統初始化時,調用了kmem_init函數創建了內核映射。我們分析該函數的具體實現: 函數void kmem_init(vm_offset_t start, vm_offset_t end) m = vm_map_create(kernel_pmap, VM_MIN_KERNEL_ADDRESS, end); 函數vm_map_create根據給定了kernel_map物理地址,創建一個新的地址映射m,而VM_MIN_KERNEL_ADDRESS和end給出了該映射范圍的下水位(lower address bound)和上水位(upper address bound)。 kernel_map = m; kernel_map->system_map = 1; (void) vm_map_insert(m, NULL, (vm_offset_t) 0, VM_MIN_KERNEL_ADDRESS, start, VM_PROT_ALL, VM_PROT_ALL, 0); vm_map_unlock(m); 由於函數kmem_init僅用於系統初始化,創建內核地址映射,因此,將獲得的地址映射賦給全局變量kernel_map保存,通過vm_map_insert函數創建一個vm_map_entry實體記錄相關值,VM_PROT_ALL和VM_PROT_ALL標識這段虛擬地址的訪問權限,參見/sys/vm/vm.h定義。 2.1 Wired (nonpageable,不可被pageout調度的頁)分配函數 固定頁(wired page)是從來不會產生頁錯誤(page fault)。其分配是由kmem_alloc函數和kmem_malloc函數實現的。 函數vm_offset_t kmem_alloc(vm_map_t map, vm_size_t size) 該函數用於在內核地址映射或子映射中,分配內存。 size = round_page(size); 調整申請內存的尺寸,使之為PAGE_SIZE的整數倍。 vm_map_lock(map); if (vm_map_findspace(map, vm_map_min(map), size, &addr)) { vm_map_unlock(map); return (0); } offset = addr - VM_MIN_KERNEL_ADDRESS; vm_object_reference(kernel_object); vm_map_insert(map, kernel_object, offset, addr, addr + size, VM_PROT_ALL, VM_PROT_ALL, 0); vm_map_unlock(map); 在vm_map結構的lock鎖機制保護下。通過調用vm_map_findspace函數查詢map地址映射是否有足夠的空間滿足申請的內存尺寸,如果成功,則可用空間的起始地址存於addr中;如果失敗則返回1,則kmem_alloc函數調用失敗。獲得該內存分配空間起始地址與VM_MIN_KERNEL_ADDRESS的偏移。通過vm_object_reference函數對內核對象kernel_obj計數器ref_count加1,kernel_obj的初始化是在VM初始化時,調用vm_object_init實現的,其類型是OBJT_DEFAULT。調用vm_map_insert函數將剛找到的虛擬地址空間插入地址映射map的vm_map_entry鏈表中。 for (i = 0; i < size; i += PAGE_SIZE) { vm_page_t mem; mem = vm_page_grab(kernel_object, OFF_TO_IDX(offset + i), VM_ALLOC_ZERO VM_ALLOC_RETRY); if ((mem->flags & PG_ZERO) == 0) pmap_zero_page(mem); mem->valid = VM_PAGE_BITS_ALL; vm_page_flag_clear(mem, PG_ZERO); vm_page_wakeup(mem); } 接下來的這段代碼非常有意思,對於申請的內存空間每一頁,通過調用vm_page_grab函數,查看該頁是否已經被kernel_object持有,如果是,則根據vm_page成員flags標識,如果是PG_BUSY,則等待該標識清PG_BUSY,將該頁重新設置為PG_BUSY,返回該地址映射(mem),如果該頁沒有被kernel_object持有,則分配一個新頁(mem)。通過判斷PG_ZERO標識,保證該頁已經清零。最後通過vm_page_wakeup函數,給正在等待該頁分配的線程一個喚醒的機會。這段代碼在查找kernel_object持有頁時,采用了自頂向下的展開算法(Sleator and Tarjan's top-down splay algorithm)。 (void) vm_map_wire(map, addr, addr + size, FALSE); 設置該段內存是wired。 函數kmem_alloc是非常低層的,一般和平台相關性的函數在申請內存會調用該函數,比如sysarch()。通常kmem_alloc是使用kernel_map地址映射和kernel_object VM對象。 函數vm_offset_t kmem_malloc(vm_map_t map, vm_size_t size, int flags) 該函數同樣是用於在內核地址映射或子映射中,分配內存。區別是: a) kmem_alloc函數在不能獲得內存時,可以阻塞等待,而在中斷層,分配內存是不能阻塞的,因此需要kmem_malloc以M_NOWAIT標識調用。 b) kmem_malloc函數為malloc調用(malloc(9))提供一種實現機制,即:在內核需要動態分配內存(malloc)時,當申請尺寸大於其閥值,最終通過kmem_malloc實現空間分配。 c) kmem_alloc路徑是使用地址映射kernel_map和對象kernel_object;而kmem_malloc路徑是使用kernel_map的子映射kmem_map和對象kmem_object,後者的具體討論在後一節說明。 size = round_page(size); addr = vm_map_min(map); 首先,根據入參調整、設置尺寸和地址。 vm_map_lock(map); if (vm_map_findspace(map, vm_map_min(map), size, &addr)) { vm_map_unlock(map); if (map !=