转一个solaris虚拟内存管理的wiki
Virtual Meory management Wikipedia,自由的百科全書
1. 內存管理 1.1. 虛擬地址空間 1.1.1. 概述 Solaris的進程地址空間分配分為兩個階段:內核地址空間的分配和用戶地址空間的分配。內核地址空間的分配只在系統啟動時進行一次,當創建第一個內核進程,該進程的地址空間就是剛剛分配好的內核地址空間,而且之后所有的內核進程都共享該內核地址空間。在Solaris中,只有內核地址空間是經過分配得到的,之后利用fork進行進程創建時,都是直接復制父進程的地址空間。第一個用戶進程創建時,它會復制父進程的地址空間,也就是內核地址空間。 1.1.2. 數據結構 1.1.2.1. 重要數據結構間關系 圖1 數據結構關系圖 每個用戶進程都擁有自己獨立的地址空間,而所有的內核進程則共享唯一的內核地址空間。每個地址空間將若干segment driver封裝起來,這些segment driver以AVL樹的形式組織起來。每個地址空間都要有hat結構,在Solaris中,硬件地址轉換(Hardware Address Translation)實現利用hat數據結構來存放一個地址空間的頂級地址轉換信息。當segment driver試圖操作硬件MMU時,HAT層會被調用。如果一個segment driver試圖創建或者撤銷某個地址空間映射,它就會調用相應的HAT函數。而htable結構則用來描述硬件頁表。 1.1.2.2.as as結構是進程地址空間結構,每一個進程都對應一個as結構的變量。as結構在uts/common/vm/as.h中定義,它的定義如下: struct as { /*as結構的一些屬性*/ kmutex_t a_contents; /* 保護as結構中的一些域 */ uchar_t a_flags; /* 描述as的屬性*/ uchar_t a_vbits; /* 用來收集統計信息 */ kcondvar_t a_cv; /* 被as_rangelock使用*/ struct hrmstat *a_hrm; /* 維護引用信息和修改信息 */ caddr_t a_userlimit; /* 該地址空間的最高允許地址 */ size_t a_size; /* 地址空間的大小 */ /* 與hat結構相關的字段 */ struct hat *a_hat; /* hat數據結構*/ /*與segment相關*/ struct seg *a_seglast; /* 該地址空間中上次命中的segment */ krwlock_t a_lock; /* 保護與segment相關的一些域 */ struct seg *a_lastgap; /* 由as_gap()發現的最近一次的segment */ struct seg *a_lastgaphl; /* last seg saved in as_gap() either for */ /* AS_HI or AS_LO used in as_addseg() */ avl_tree_t a_segtree; /* 地址空間中的segment,以AVL樹的形式組織 */ avl_tree_t a_wpage; /* 守護頁 (procfs) */ uchar_t a_updatedir; /* 映射改變時,重建a_objectdir */ timespec_t a_updatetime; /* 映射上一次改變的時間 */ vnode_t **a_objectdir; /* 對象目錄 (procfs) */ size_t a_sizedir; /* 對象目錄的大小*/ struct as_callback *a_callbacks; /* callback列表*/ void *a_xhat; /* xhat提供者列表 */ }; as中的字段分成三個部分。第一個部分是用于描述as的一般性字段,包括用于保護as中域的互斥變量a_contents,描述as屬性的a_flags域,用來收集統計信息的a_vbits域,用來保護as_rangelock的條件變量,以及描述該地址空間大小的size域。 第二部分是與hat結構相關的字段。每個地址空間都需要有相應的hat結構,hat結構中主要存放地址轉換信息,幫助MMU完成虛擬地址到物理地址的轉換。第三部分是與segment相關的字段。 a_seglast表示該地址空間中最近一次命中的segment。每次對地址空間的訪問都需要更新該字段。 a_lock保護與segment相關的域,當對as中某個segment相關的域操作時,都需要用該讀寫鎖對這些域進行保護。 a_lastgaphl表示在as_gap()中存放的最近一次訪問的segment。在向地址空間增加一個segment時,為了提高效率,并沒有采用avl_find()方法來尋找插入點,而是簡單地以a_lastgaphl為起點尋找插入點(如果a_lastgaphl不空)。 a_segtree表示地址空間中所有segment組成的AVL樹。在Solaris中,地址空間中的所有segment都以AVL樹的形式組織起來。這樣,對segment進行增刪查改操作效率比較高。 a_wpage表示該地址空間的守護頁。這些守護頁也就是procfs。利用守護頁可以對內存中的一些段進行監控。 a_updatedir標示地址空間的映射發生變化,通知進程需要重建a_objectdir。例如,當向進程地址空間中,新增一個segment時,需要將該地址空間的a_updatedir域置為1。 a_updatetime表示最近一次地址映射發生變化的時間。 a_objectdir表示對象目錄。 a_callbacks是callback列表。該列表中主要是掛在segment driver上的回調函數列表。 1.1.2.3.seg seg結構是描述進程中segment的結構,每一個進程地址空間都由若干segment組成,這些segment組成一個AVL樹。seg結構在uts/common/vm/seg.h中,它的定義如下: struct seg { caddr_t s_base; /* 虛擬基地址 */ size_t s_size; /* 以byte計算的segment大小 */ uint_t s_szc; /* 該段所支持的最大的頁大小 */ uint_t s_flags; /* segment的標記*/ struct as *s_as; /* 該segment從屬的地址空間 */ avl_node_t s_tree; /* 在該地址空間中,針對該segment的AVL樹鏈接 */ struct seg_ops *s_ops; /* 對該segment的操作向量*/ void *s_data; /* 針對具體實例的私有數據*/ }; 每個地址空間都包含了若干segment,這些segment由不同的segment driver管理。Seg結構包含該segment的虛擬基地址,segment的大小,指向其所屬地址空間的指針,用來維護AVL樹的指針,以及掛在該segment driver上的回調函數和數據。可以看出,在seg結構中既包含描述segment的屬性信息,也包含對該segment進行操作、訪問的函數信息。 1.1.2.4.hat hat結構是進程地址空間結構,每一個進程都對應一個as結構的變量。as結構在uts/common/vm/as.h中,它的定義如下: struct hat { /* hat結構的一些屬性 */ kmutex_t hat_mutex; /* 整個hat結構的互斥量 */ kmutex_t hat_switch_mutex; /* hat切換時的互斥量 */ struct as *hat_as; /* 指向hat所屬的地址空間 */ uint_t hat_stats; /* hat結構的統計信息 */ pgcnt_t hat_pages_mapped[MAX_PAGE_LEVEL + 1]; cpuset_t hat_cpus; uint16_t hat_flags; /* hat的標記 */ /* 維護hat鏈表的字段 */ struct hat *hat_next; /* 指向下一個hat結構 */ struct hat *hat_prev; /* 指向上一個hat結構 */ /* 與htable相關的字段 */ htable_t *hat_htable; /* 指向頂級硬件頁表的指針 */ uint_t hat_num_hash; /* htable哈希數目 */ htable_t **hat_ht_hash; htable_t *hat_ht_cached; /* 空閑的htables緩存 */ x86pte_t hat_vlp_ptes[VLP_NUM_PTES]; }; hat中的字段分成三個部分。第一部分主要描述hat結構的屬性,包括一些互斥變量,hat所屬的地址空間指針,hat結構的統計信息,及hat的標記等。第二部分則包含了如何組織hat雙向鏈表的字段。 第三部分是一些與htable相關的字段。 hat_htable是指向頂級硬件頁表的指針。當進行地址轉換,調用HAT層時,通過hat結構就可以找到頂級硬件頁表,從而進行地址轉換。 1.1.3. 情景 1.1.3.1. 內核地址空間的分配 這個情景描述如何在系統啟動時分配整個系統中的第一個地址空間---內核地址空間,其中涉及到的主要函數包括:表1 內核地址空間分配中的主要函數函數名 文件名 功能描述 kvm_init uts/i86pc/os/Startup.c 內核虛擬地址空間的初始化工作 as_avlinit uts/common/vm/Vm_as.c 為地址空間分配所需的avl樹 as_addseg uts/common/vm/Vm_seg.c 將segment添加到指定的地址空間上 as_setprot uts/common/vm/Vm_as.c 為指定區域設置映射分配內核地址空間的工作主要在kvm_init()函數中完成。這個函數的流程如圖2所示。 圖2 kvm_init()的函數流程圖 kvm_init()首先為kas(內核地址空間的as結構)創建segment AVL樹和守護頁AVL樹,該過程是由as_avlinit()函數完成的。然后kvm_init()會調用seg_attach()及segkmem_create()函數來為內核地址空間添加必要的段,如內核代碼段,kvalloc段,內核debugger段。seg_attach()函數的主要實現工作是由as_addseg()函數完成的。as_addseg()函數的流程如圖2所示。 圖3 as_addseg()的流程圖 在as_addseg()函數中,為newseg尋找插入點時,為了提高效率,并沒有直接利用avl_find()來尋找插入點,而是先利用as的a_lastgaphl,該域存放的是as_gap()函數最近使用的segment。如果a_lastgaphl不為空,那么則以它作為初始點,為newseg尋找合適的插入點,如果找到符合條件的插入點,則調用avl_insert_here()函數將newseg插入到AVL樹中;如果a_lastgaphl為空,則利用avl_find()等函數來尋找符合條件的插入點。最后還需要判斷所找到的插入點表示的段與newseg段是否有重合,如果有重合,在sparc處理器下就需要調用seg_unmap()函數取消插入點所表示的段的映射,之后調用avl_insert()函數將newseg插入到AVL樹中。 利用seg_attach()及segkmem_create()函數來為內核地址空間添加好內核代碼段,kvalloc段及內核debugger段之后,kvm_init()調用as_setprot()來對Red Zone、內核代碼段及內核數據段設置相應的訪問權限。在這里,為了確保Red Zone域不為訪問,將其權限置為0;將內核代碼段的權限置為可讀/可寫/可執行;將內核數據段的權限置為可讀/可寫/可執行。圖4將給出as_setprot()函數的流程: 1.1.3.2.虛擬地址空間的釋放 這個情景描述如何釋放一個虛擬地址空間,其中涉及到的主要函數包括:表2 虛擬地址空間釋放中的主要函數函數名 文件名 功能描述 as_free uts/common/vm/Vm_as.c 釋放虛擬地址空間 hat_free_end uts/i86pc/vm/Hat_i86.c 進程地址空間正被銷毀,該函數將銷毀相應的hat xhat_free_end_all uts/common/vm/Xhat.c 銷毀相應的xhat 釋放虛擬地址空間的工作主要在as_free ()函數中完成。這個函數的流程如圖4所示: 圖4 as_free()的流程圖 as_free()函數將釋放一個地址空間,它的具體過程: 1. 它會調用as_do_callback()函數將所有的回調函數執行完畢,將回調函數列表清空 2. 進行hat結構釋放的開始工作 a) 首先將as的flag置為AS_BUSY,以阻止新的XHAT被附到as上 b) 調用hat_free_start來設置hat_flag為HAT_FREEING(在銷毀HAT時,需要設置該標志),為釋放hat做好準備 c) 調用xhat_free_start_all,主要的工作就是將xhat鏈表中的holder都設為curthread,為釋放xhat列表做好準備 3. 釋放as中的segment a) 利用SEGOP_UNMAP來取消對as中各段的映射,并會判斷其返回值err。若err=0表明正常執行;否則err==EAGAIN(表明當前段資源不可用),出現該err有兩種情況:callback未處理完,內存被加鎖,要進行等待。并回到步驟1重新進行。 4. 進行hat結構釋放的結尾工作 a) 調用hat_free_end來真正清除相應的hat結構? 確保hat當前沒有被page table stealing,然后從hat鏈表中刪除hat結構,并重置kas的hat鏈表? 將所有htables都釋放? 利用kmem_cache_free來釋放hat結構所占用的cache b) 調用xhat_free_end_all來釋放XHAT 5. 釋放as結構本身所占的空間 a) 釋放object directory(procfs)中的vnode b) 利用kmem_cache_fee釋放as結構 1.1.3.3.虛擬地址空間的復制 這個情景描述如何復制一個虛擬地址空間,其中涉及到的主要函數包括:表2 虛擬地址空間復制中的主要函數 函數名 文件名 功能描述 as_dup uts/common/vm/Vm_as.c 復制虛擬地址空間 seg_alloc uts/common/vm/Vm_seg.c 分配一個段,并將它附到相應的地址空間上 hat_dup uts/sfmmu/vm/hat_sfmmu.c 復制地址空間的地址轉換 xhat_dup_all uts/sfmmu/vm/Xhat.c 復制地址空間的xhat列表復制虛擬地址空間的工作主要在as_dup ()函數中完成。這個函數的流程如圖2所示: 圖5 as_dup流程圖 as_dup將復制一個地址空間,具體過程如下: 6. 調用as_alloc()為新的地址空間分配一個與之對應的as數據結構 7. 將原來地址空間所有的段都拷貝到新的地址空間中(利用一個循環) a) 調用seg_alloc()來分配一個段,并將這個新分配的段映射到新的地址空間中 b) 如果上面的seg_alloc()工作失敗,則會調用as_free()將新的地址空間釋放 c) 調用SEGOP_DUP()將原來段的操作復制到剛剛分配好的段中 d) 若復制段操作時發生錯誤,需調用seg_free()將剛剛分配的段釋放,然后調用as_fee()將新的地址空間釋放 8. 調用hat_dup()進行hat的復制工作 9. 調用xhat_dup_all()進行xhat的復制工作這里需要指出,hat_dup()和xhat_dup_all()只進行了一些字段取值的驗證,并沒有去進行實際的復制工作,這里hat和xhat都采用copy-on-write的策略完成復制。也就是說,只有真正去使用hat或xhat時,才會去進行復制工作。 1.2. 匿名內存 1.2.1. 概述 Solaris匿名內存是由segvn管理的,但并不直接與文件相關聯,匿名內存用于進程的棧,堆以及寫入時拷貝頁。匿名頁是通過anon層接口被創建的。當一個段第一次收到一個頁錯誤,它分配一個anon映像結構(該結構說明anon頭部在哪里)并在匿名映射的amp域里置入指向該anon頭部的指針。然后分配插槽數組,需要足夠大以至于能放下段內潛在的其他頁。插槽數組采用一次間接尋址和兩次間接尋址(一維數組和二維數組),這取決于需要插槽的數目。 32位系統由于要支持大于16MB的段,需要兩次間接尋址;64位系統,因為指針的長度更長,當支持大于8MB時需要兩次間接尋址。當只用一次間接尋址時,anon頭部的 anon_chunk直接引用了anon的插槽數組。當我們使用兩次間接尋址時,anon_chunk被分成兩大塊:針對32位系統,由2048個插槽組成的插槽塊和針對64位系統,由1024個插槽組成的插槽塊。這一分配過程由anon層接口anon_create實現。每一個anon插槽指向一個anon結構,anon結構描述了與地址空間內一頁大小的區域內容一致的虛擬內存頁。 使用匿名內存會有很多的優點。例如,當創建一個進程時,被創建的進程的所有地址都映射到物理內存的相同位(相同的頁)。但是如果子進程在此時要對內存做些不同的操作(例如子進程管理內存中的一個數組),vm子系統會將這些頁復制,并在子進程中改變映射指向新的頁。這些新頁即為匿名內存,子進程可以正常的修改數組,而不必知道這個數組已經有了一個新的物理內存了。這保證了存儲部分對子進程的透明。 1.2.2. 數據結構 1.2.2.1.重要數據結構之間的關系 1.2.2.2.anon結構 struct anon { struct vnode *an_vp; /* vnode of anon page */ struct vnode *an_pvp; /* vnode of physical backing store */ anoff_t an_off; /* offset of anon page */ anoff_t an_poff; /* offset in vnode */ struct anon *an_hash; /* hash table of anon slots */ int an_refcnt; /* # of people sharing slot */ }; 每個匿名頁,不論其在內存還是在對換區,都有一個anon結構。這個結構(也稱為插槽)提供了匿名頁和其后備存儲器之間的一個間接級的映射。(an_vp,an_off)為這個插槽指向匿名頁的vnode,(an_pvp,an_poff)指向這個插槽代表的頁的物理存儲器的位置。An_hash是anon插槽的一個散列表。這個列表由相關的匿名頁的(an_vp,an_off)進行散列,并且提供一個方法用于從一個匿名頁轉到相關的匿名anon插槽。An_refcnt用于一個引用計數,記錄了在寫入時拷貝的情況下,需要建立的各個分散的拷貝的數目。一個大于零的refcnt保護了插槽的存在。在anon_alloc被調用時,refcnt初始化為1 。 1.2.2.3.anon_hdr結構 struct anon_hdr { kmutex_t serial_lock; /* serialize array chunk allocation */ pgcnt_t size; /* number of pointers to (anon) pages */ void **array_chunk; /* pointers to anon pointers or chunks of */ /* anon pointers */ int flags; /* ANON_ALLOC_FORCE force preallocation of */ /* whole anon array */ }; anon數組指針在塊中分配。每個塊都有anon指針的pagesize/sizeof(u_long*),anon_hdr結構指向匿名數組,控制著匿名插槽的分配。 anon數組是二維指針數組比一個塊要大。第一級指針指向anon數組的塊,第二級包含anon指針的塊。如果anon數組比一個塊要小則建立整個的anon數組。如果anon數組比一個塊要大則僅僅第一維數組被分配。則另一維數組只在它們被anon指針初始化時分配。serial_lock,分配一個連續的數組塊。Size,記錄指向匿名頁的指針的數目。array_chunk,指向anon指針的指針。 1.2.2.4.anon_map結構 struct anon_map { krwlock_t a_rwlock; /* 保護anon_map結構和anon數組*/ size_t size; /* anon數組大小*/ struct anon_hdr *ahp; /*anon數組的頭指針, 包含anon指針數組*/ size_t swresv; /* 為anon_map結構而保存的交換空間 */ uint_t refcnt; /* 這個結構的引用計數 */ ushort_t a_szc; /*在共享的進程中最大的 szc */ void *locality; /* lgroup locality info */ }; 這是匿名內存中最核心的數據結構。 Anon_map結構用于各種各樣的anon從客戶端來進行匿名內存的管理。當匿名內存被共享時,不同的共享客戶端將會指向同一個anon_map結構。同樣的如果一個段在anon_map結構存在中間沒有被安排,則新創建的段將仍會共享anon_map結構,盡管這兩個段用到的是anon數組的不同范圍。 1.2.3.情景 1.2.3.1.匿名內存的分配 所用的函數: anonmap_alloc()為給定交換區的相關段分配并初始化一個anon_map結構. anon_create()創建指針列表. anon_get_slot()從列表中返回指定的匿名索引的指針。 anon_alloc()分配一個anon插槽,上鎖后返回該插槽. Anon_zero()分配一個私有的用零填充的anon頁 Anon_set_ptr()用一給定的指針設置列表項,該指針為一指定偏移量的指針. 流程圖如下: 詳細的說明: 1. 進入anonmap_alloc()函數,為給定交換區的相關段分配并初始化一個anon_map結構 2. 進入anon_create()函數,來創建指針列表,指向匿名數組(一維或二維) 3. 創建工作結束,進入匿名數組的操作,首先通過page_get_pagecnt()函數來得到可用匿名插槽的數目,如果可用插槽數目為0則將其標記為忙碌,若大于0則調用anon_array_lock()來鎖定插槽區域,獲取第一個插槽,并將此插槽置位忙碌,之后用anon_get_slot()來獲取一個匿名插槽。 4. 調用Anon_zero()來分配一個私有的用零填充的anon頁,在此函數中,用anon_alloc()來分配匿名插槽和它的鎖。通過[vp,offset]來尋找頁(page_lookup()),沒有找到,所以要調用page_lookup_create()來建立一個頁,并將其置位vp,offset。 5. 從頁空閑列表中根據所給的vp和offset選擇一個最合適的頁(page_get_freelist()),并將此頁上鎖(page_trylock()),并將此頁從空閑列表中移出(page_sub()),插入到散列表中(page_hashin())。請求此頁的i/o鎖(page_io_lock()),并將此頁插入到引用列表中(page_add)。 6. 調用Anon_zero()來分配一個私有的用零填充的anon頁,在此函數中,用anon_alloc()來分配匿名插槽和它的鎖。通過[vp,offset]來尋找頁(page_lookup()),沒有找到,所以要調用page_lookup_create()來建立一個頁,并將其置位vp,offset。 7. 從頁空閑列表中根據所給的vp和offset選擇一個最合適的頁(page_get_freelist()),并將此頁上鎖(page_trylock()),并將此頁從空閑列表中移出(page_sub()),插入到散列表中(page_hashin())。請求此頁的i/o鎖(page_io_lock()),并將此頁插入到引用列表中(page_add)。 8. 將此頁添0(pagezero()),將此頁鎖的等級從獨占鎖降為共享鎖(page_downgrade()). 9. 若有匿名頁,也將此匿名頁添0(anon_zero()),并用給定的指針設置列表項(anon_set_ptr()),通過hat層從實際的內存空間中分配此頁(在對換區域中)。退出匿名內存數組的操作(anon_array_exit())。 10. 至此,所有的操作完成。一個問題要注意:為什么anon結構要設置成一個數組?答案:每個anon結構代表了一個內存頁。而每一個段可能比一頁的大小要大,所以需要不止一個的anon結構去描述它,所以我們需要一個數組。 1.2.3.2.匿名內存的釋放 涉及到的函數: anonmap_free() 釋放anon結構 anon_release() 釋放匿名數組的指針 lgrp_shm_policy_fini()為anon_map結構釋放共享內存的決策樹和為零的本地空間。此函數在Lgrp.c文件中。流程圖: 詳細解釋: 1. 判斷anon_map結構中的ahp指針是否為空,若為空,則說明此anon頁已經被釋放,錯誤。若不為空則進入下一步。 2. 判斷匿名頁是否還有進程在引用,若refcnt=0則說明沒有進程在引用,則可以釋放。若refcnt!=0則錯誤。 3. 釋放在物理內存中的頁面 4. 釋放匿名數組,要分為兩種情況,即一維數組和二維數組的情況。 5. 清空對象緩存。 1.2.3.3. 分配一個anon插槽 函數anon_alloc() 流程圖: 詳細說明:anon_alloc參數為vnode和其偏移。首先聲明一個anon結構類型變量ap,ap是一個anon插槽。為ap在緩存中分配空間,若失敗則調用swap_alloc進行分配,若成功則將ap的兩個分量an_vp和an_off置為參數的值,并將ap的其他分量都初始化。根據an_vp和an_off進行散列,并插入散列表中并上鎖,最后返回指針ap。 1.2.3.4.匿名數組的拷貝 函數:anon_copy_ptr():拷貝anon數組sahp到另一個anon數組dahp. 流程圖: 詳細過程:拷貝anon數組sahp到另一個anon數組dahp。幾個參數的意義,s_idx指sahp數組塊的大小,d_idx指dahp數組塊的大小,npages指復制需要的頁面數。函數的執行過程:如果兩個數組都是一維的,要對sahp和dahp所指向的空間進行測試,(1)如果其小于ANON_CHUNK_SIZE的值才是合法值,然后調用bcopy函數對數組進行復制。(2)如果兩個數組都是二維的則相對要麻煩一些。同一維數組一樣,首先要測試sahp和dahp所指向的空間的大小是否合法,然后不斷測試npage的值,當其值大于零的時候則進入循環,相當于調用若干次的bcopy對若干個一維數組進行復制,最終得到二維數組的復制。(3)如果至少有一個數組是二維的,則在npage大于零的情況下,要求anon索引的指針不能為空,用anon_get_ptr函數得到索引指針并且放在ap中,并通過anon_set_ptr函數直接復制給dahp。 1.2.3.5.設置指針數組結構 函數:anon_create() 流程圖: 詳細說明:這個函數可以分配和回收指針,返回和設置給定的偏移的指針數組的入口。首先在內核內存中分配數組指針的空間,對數組指針的互斥鎖進行初始化。若數組是一維的,整個指針數組占npages個頁,則在內核內存中分配這npages個頁的anon指針的空間(用函數kmem_zalloc),若沒有分配成功則調用kmem_free尋找內核中的空閑區。若是二維數組,則只是在計算指針數組大小的時候麻煩一下,其余均一樣。最后返回這個數組的指針(anon_hdr*類型)。這里要說一下kmem_free函數的算法,將內核內存中空閑的塊建立為一個堆,從根部開始在其中查找與給定塊最相近的空閑塊,如果找到,則將給定的塊寫入到空閑塊中。 1.2.3.6.匿名頁引用計數的操作 函數:Anon_decref():對anon頁的引用減1,如果引用計數為零,則將其和其相關頁釋放。流程圖: 詳細過程:在散列表中找到ap指向的匿名頁,并通過臨界區訪問此頁,以保證互斥。(1)若引用此頁的計數不為零,則對其進行減1操作,如果減1以后對此匿名頁的引用變為零,則調用函數swap_xlate(),將一個匿名插槽轉換為其相關的vnode和vnode中的偏移,并查找此匿名插槽的頁,如果找到,則調用VN_DISPOSE函數將與之相關的匿名頁歸還到空閑列表中,以表示此頁真正成為空閑頁了。(2)若引用計數為零,則直接從散列表中將ap所指的匿名頁移除,如果ap_pvp所指的物理頁不為null則也要調用函數swap_phys_free將物理頁釋放。 1.2.3.7.匿名內存中數據的復制函數:anon_dup():復制anon頁size字節長度的內容。流程圖: 詳細過程:首先,調用btopr函數將要復制的長度size轉換為以頁為單位的度量即npages個頁。每復制一頁就將npages減1,找到index處開始第一個合法的anon指針賦給ap。然后,調用anon_set_ptr函數將old的內容復制到new處,并對每一個匿名頁的引用數都加1。函數中用off來記錄每頁中的偏移,進行復制。須注意,對頁的引用計數進行修改時要保證互斥進行,代碼如下: mutex_enter(ahm); ap->an_refcnt++; mutex_exit(ahm); 1.2.3.8.釋放一組anon頁 函數:anon_free():釋放一組anon頁,長度為size字節,并清空指向這些anon表項的指針。流程圖: 詳細過程:首先將size字節的長度轉換為以頁為單位,npages個頁。即轉換為釋放這npages個頁的函數。找出index后的第一個合法的anon指針ap,ap指向的是要釋放的第一頁。將此頁的列表項設置為null,調用anon_decref函數減少一個此頁的引用計數。當該引用的計數為0時,釋放該頁和與之關聯的頁(如果有的話)。將npages減1再進入循環,直至npages等于零,表明所有的匿名頁已被釋放。 1.3.Swap文件系統 1.3.1.概述現代操作系統都實現了“虛擬內存”這一技術,在功能上突破了物理內存的限制,使程序可以操縱大于實際物理內存的空間。有兩個基本的虛擬內存管理模型:交換(swapping)和按需換頁(demand pages)。交換模型的內存管理粒度是進程,當物理內存不足時,最不活躍的進程被換出內存。按需換頁的內存管理粒度是頁面,在內存匱乏時,只有最不經常使用的頁面被換出。Solaris中使用了這兩種虛擬內存管理方法。通常情況下使用按需換頁方式,在內存嚴重不足的時候采用交換的方式。在進行虛擬內存管理的時候,在系統的磁盤空間中,必須專門劃分出一個部分作為系統的Swap空間。Swap空間的作用可簡單描述為:當系統的物理內存不夠用的時候,就需要將物理內存中的一部分空間釋放出來,以供當前運行的程序使用。那些被釋放的空間可能來自一些很長時間沒有什么操作的程序,這些被釋放的空間被臨時保存到Swap空間中,等到那些程序要運行時,再從Swap中恢復保存的數據到內存中。這樣,系統總是在物理內存不夠時,才進行Swap交換。需要聲明的是,并不是所有從物理內存中交換出來的數據都會被放到Swap空間中,有相當一部分的數據直接交換到操作系統的文件系統中了。例如,有的程序會打開一些文件,對文件進行讀寫(其實每個程序都至少打開一個文件,那就是運行程序本身),當這些程序的內存空間需要交換出去時,文件部分的數據就沒有必要放到Swap空間中了,如果是讀文件操作,那么內存數據直接就釋放了,不需要交換出來,因為下次需要時,直接從文件系統就能恢復;如果是寫文件,只需要將變化的數據保存到文件中,以便恢復。但是那些用malloc( )和new等函數生成的對象的數據則不同,需要Swap空間,因為它們在文件系統中沒有相應的“儲備”文件,因此被稱為“匿名”(Anonymous)的內存數據,這類數據還包括堆棧中的一些狀態和變量數據等,所以說,Swap空間是“匿名”數據的交換空間。內存的每個物理頁面都由它的vnode和offset指定。當所要尋找的頁面不在內存中時,則vnode和offset指出該頁在后備存儲器中的位置。對于一個文件來說,物理頁緩存了文件的vnode和offset.交換空間就像一個后備存儲器,存儲內存的匿名頁,因此,當內存不足時,可以把內存的某頁交換到磁盤中,以增加內存空閑空間。因為交換空間是作為匿名內存的后備存儲器,所以我們必須首先確定是否有足夠的交換空間,以便可以把頁面交換出去。因此,在創建一個可寫入的映象前,我們要先申請交換空間。當有足夠的內存空間可裝入進程的內容時,Solaris內核允許匿名內存可不申請交換空間。這意味著在某些情況下,一個系統可以在很少或沒有交換空間下運行。通常情況下,Swap空間應大于或等于物理內存的大小,最小不應小于64M,通常Swap空間的大小應是物理內存的2-2.5倍。對于傳統的UNIX,在可寫入的虛擬內存中,其每個頁大小的單元都需要一個同等大小的交換空間。比如,在傳統的UNIX系統里,一個malloc要求分配8 Mbytes空間,那同時,它也要申請8 Mbytes的交換磁盤空間,盡管這些交換空間它可能從未用到。一般地,粗略估計進程所要的空間的大小是其所使用的物理頁的兩倍,這就導致了交換空間要等于兩倍大小的內存空間。而swapfs層允許Solaris在分配時更加謹慎,我們只需要讓交換空間等于虛擬內存的大小即可,該虛擬內存比機器中可用的可分頁物理內存大。 Solaris的交換使用swapfs來實現交換區域分配,以提高空間的使用效率。swapfs文件系統位于anon層和物理交換設備之間,是一個虛擬的文件系統。即使沒有分配物理交換空間,swapfs文件系統也使每個頁面如同有真實的后備交換空間。 swapfs文件系統使用一個全局變量:availrmen來跟蹤系統中可用且可分頁的物理內存,并把添加到可使用的交換空間中。當我們申請虛擬交換時,我們只是簡單地減少了可用的虛擬內存的總量。只要有充足的內存和物理交換空間可用,則該交換分配就是成功的。物理交換空間直到需要使用時才會分配。當我們創建一個私有段時,我們申請交換分區和分配anon結構。在這狀態中,直到一個真正的內存頁作為ZFOD或copy-on-write的結果被創建時,anon結構的分配等才會真正發生。當一個物理頁默認加入時,用vnode/offset對它進行標記。在Solaris中,當段驅動調用anon_alloc()去獲取一個新的匿名頁時,該匿名頁被分配swapfs vnode和offset。anon_alloc()函數通過swafs_getvp()進入swapfs,然后調用swapfs_getpage()創建一個帶有vnode/offset的新頁面。an_vp和an_off被初始化成swapfs虛擬交換設備的vnode和offset,這兩個an_vp和an_off是在anon結構中用來標明該頁的后備存儲的地址的。 在沒有page-out請求之前,并不需要任何物理交換空間。當段請求虛擬交換空間時,可用的虛擬交換空間的總量減少,但是,因為我們還不需要把頁置換到物理交換區,所以,物理交換空間并沒有被分配。當發生第一個page-out請求時,真正的交換分區才被分配。這時,頁掃描器為該頁檢查vnode,然后調用putpage()方法。因為該頁的vnode是swapfs vnode,因而調用swapfs_putpage()把該頁置換到交換設備。swapfs_putpage()分配物理交換分區的一個頁大小的塊給該頁面,然后把anon槽中的vnode an_pvp和an_poff設置為指向物理交換設備,接著該頁就被置換到交換設備中了。 另外,系統中有可能包含多個Swap分區,分區的數量對性能也有很大的影響。因為Swap交換的操作是磁盤I/O的操作,如果有多個Swap交換區,Swap空間的分配會以輪流的方式操作于所有的Swap,這樣會大大均衡I/O的負載,加快Swap交換的速度。 1.3.2.數據結構 1.3.2.1.重要數據結構關系 1.3.2.2.swapinfo 每個交換空間都有一個swapinfo結構來記錄該區域的相關信息。這些結構連成一個線性表,決定交換空間在邏輯交換設備中的順序。每個結構包含一個指針,指向相應的位圖,同時也說明該交換空間的大小,和它相應的vnode。Swapinfo結構在/uts/common/sys/swap.h中,它的定義如下: struct swapinfo { ulong_t si_soff; /*指明文件開始的偏移量*/ ulong_t si_eoff; /* 指明文件結束的偏移量*/ struct vnode *si_vp; /*指向一個結點*/ struct swapinfo *si_next; /*指向下一個交換空間 */ int si_allocs; /*該交換空間的分配結果*/ short si_flags; /* 標記*/ pgcnt_t si_npgs; /* 交換空間的頁面數 */ pgcnt_t si_nfpgs; /* 交換空間的空閑頁面數*/ int si_pnamelen; /*交換文件的名字長度加一 */ char *si_pname; /* 交換文件的名字*/ ssize_t si_mapsize; /*為位圖分配的字節 */ uint_t *si_swapslots; /*插槽的位圖,未置位標明該插槽為空*/ pgcnt_t si_hint; /*空閑頁的第一頁 first page to check if free */ ssize_t si_checkcnt; /*尋找空的插槽 # of checks to find freeslot */ ssize_t si_alloccnt; /* 用來獲取ave結果used to find ave checks */ }; ulong_t si_soff和ulong_t si_eoff分別指明文件在vnode中的開始地址和結束地址。 vnode *si_vp指向某設備的vnode。 swapinfo *si_next指向下一個交換空間,從而把所有的交換空間串連起來。 si_allocs指出交換空間的分配結果,si_flags是一個標記,以后會有詳細的定義。 pgcnt_t si_npgs和pgcnt_t si_nfpgs分別指出交換空間的頁面數和空閑頁面數 si_pnamelen的值為交換文件的名字長度加一 si_pname指出交換文件的名字 si_mapsize指出交換空間對應的位圖的比特數,系統中,每個物理交換空間都有一個相對應的位圖,該位圖用來表示它的物理容量。位圖記錄了哪個交換槽已經被使用或尚未使用。分配時是通過遍歷位圖來找到第一個空閑槽。因而,在交換設備中的偏移量和插槽所支持的頁面地址之間不存在線性關系。相反,這是一個一對一的映像。 si_swapslots插槽的位圖,未置位則標明該插槽可用。 si_hint指出空閑頁的第一頁; si_checkcnt尋找空的插槽 si_alloccnt用來獲取ave結果,ave為一個宏,表明空間分配了多長時間。 1.3.2.3. swapre swapre結構指出將要進入或移出交換空間的資源的路徑。Swapre結構在/uts/common/sys/swap.h中,其定義如下: typedef struct swapres { char *sr_name; /* 特定資源的路徑名*/ off_t sr_start; /*被交換資源的起始偏移地址g*/ off_t sr_length; /*交換空間的長度 */ } swapres_t; sr_name指出將要移入或移出交換空間的資源的路徑名稱 sr_start 指出被交換資源的起始偏移地址 sr_length指出交換空間的長度 1.3.2.4.swapent swapent結構保存交換文件(相當于交換設備,暫存換出頁面)的名字,將要進行交換的頁數等,它在/uts/common/sys/swap.h中,其結構定義如下: typedef struct swapent { char *ste_path; /* 獲取交換文件的名字 */ off_t ste_start; /* 交換的起始塊*/ off_t ste_length; /*交換空間的長度 */ long ste_pages; /* 可交換的頁數*/ long ste_free; /*空閑的交換頁數 */ int ste_flags } swapent_t; ste_path獲取交換文件的名字 ste_start指出交換的起始塊 ste_length交換空間的長度 ste_pages可以進行交換的頁數 ste_free 空閑的交換頁數 1.3.2.5.swaptable swaptable結構是一個數組,存儲swapent,指出有多少個交換文件。它在/uts/common/sys/swap.h中,其結構定義如下: typedef struct swaptable { int swt_n; /*指出有多少個交換文件*/ struct swapent swt_ent[1]; /* array of swt_n swapents */ } swaptbl_t; 1.3.3.情景 1.3.3.1.從設備中分配交換頁 表6 添加交換文件時用到的主要函數函數名 文件名 功能描述 swap_phys_alloc uts/common/vm/Vm_swap.c 分配指定大小的連續頁 swap_getoff uts/common/vm/Vm_swap.c 獲取設備中空閑頁的開始偏移量從物理設備中分配交換頁是通過函數swap_phys_alloc來完成的, 這個函數的流程如圖所示: 圖11 swap_phys_alloc函數流程圖 分配指定大小的,連續的物理交換頁。分配成功返回1,若一個頁面都分配不了,則返回0。首先對swapinfo表加鎖。然后在列表中逐個搜索,搜索有空閑頁的設備。如果調用者表明交換頁不從某個設備中分配,則交換頁應從其他設備中分配,于是應尋找不相同的設備,如果找到,轉到found;如果調用者沒有這種需求,則直接轉到found。sip指向所找到設備的swapinfo。調用swap_getoff,找到設備空閑頁的偏移地址soff。設備的空閑頁數減掉1。如果soff為-1,出錯。開始從設備中分配所需的頁面數。若分配過程中,設備的頁已分配完或剩下的比特數不夠一頁大小,則完成分配過程,盡管可能還未分配夠要求的頁數。把設備的vnode,偏移量soff,剛才分配的比特數len等返回給調用者。如果設備分配的交換頁數超過swap_maxconfig(一個宏,在anon_init中定義其大小),為了把負載平衡分配到各個設備,把該設備的si_allocs設為0,并且如果該設備排在鏈表的最后,把silast的值改為swapinfo鏈表的開頭結點。保存分配的信息,完成程序。 1.3.3.2.釋放物理交換頁 表7 釋放物理頁時用到的主要函數 函數名 文件名 功能描述 swap_phys_free uts/common/vm/Vm_swap.c 釋放指定設備的物理頁從指定設備中釋放頁是通過函數swap_phys_free來完成的, 這個函數的流程如圖所示: 圖12 swap_phys_free函數流程圖釋放一個物理頁。調用者給出欲釋放頁所在設備的vnode,頁的偏移量和頁大小。調用者給出的欲刪頁大小,并不符合設備中頁的規格,即調用者眼里的一頁和設備中的一頁大小是不一樣的。 首先,在swapinfo鏈表中開始查找。如果某個結點的vnode和指定vnode相同,且給出的偏移量超出結點范圍,則計算在指定偏移量之前的頁數pagenumber, 而npage是pangnumber和釋放頁數之和。 釋放每一頁。如果某頁對應的位圖表示該頁未曾使用,則打印:“釋放空閑頁。”把該頁對應的位圖清0,設備的空閑頁數加一。 1.3.3.3.釋放一個正在使用的交換頁 表8 釋放一個正在使用的交換頁用到的主要函數函數名 文件名 功能描述 swapslot_free uts/common/vm/Vm_swap.c 釋放指定設備的正在使用的物理頁 VOP_GETPAGE /on/usr/src/uts/common/sys/vnode.h 找到指定頁,并設置該頁屬性 swap_anon uts/common/vm/Vm_swap.c 找尋和指定vnode,off相關的anon swap_phys_free uts/common/vm/Vm_swap.c 釋放指定的交換頁頁 hat_setmod /on/usr/src/uts/common/vm/hat.h 為指定頁設置指定的屬性從設備中釋放正在使用的交換頁是通過函數swapslot_free來完成的, 這個函數的流程如圖所示: 圖13 swapslot_free函數流程圖釋放某個設備正被使用的交換頁,調用者需提供該設備的vnode,欲釋放頁在設備中的偏移量,和設備的swapinfo結點。使用VOP_GETPAGE獲取欲釋放的頁,如果VOP_GETPAGE返回錯誤信息,轉出錯處理。對找到的頁加鎖,添加互斥量。找到欲釋放頁所在的anon結構,把anon結構返回給ap;如果找不到相應的anon,出錯處理;如果ap的后備存儲的vnode和指定設備的vnode相同,且ap在設備中的偏移量沒超出設備的范圍,則調用swap_phys_free釋放該頁。把ap的an_pvp和an_poff置為空,調用hat_setmod對該頁屬性進行設置。 1.3.3.4.添加交換文件 表4 添加交換文件時用到的主要函數 函數名 文件名 功能描述 swapadd uts/common/vm/Vm_swap.c 增加新的交換設備到列表中,并在更新anoninfo計數器前轉移分配給它 common_specvp uts/common/fs/specfs/specsubr.c 對給定設備的vnode,函數返回和該設備vnode相關聯的通用vnode格式 mutex_enter /on/usr/src/lib/libzpool/common/kernel.c 對文件上鎖 mutex_exit /on/usr/src/lib/libzpool/common/kernel.c 對文件開鎖 kmem_zalloc /on/usr/src/uts/common/os/kmem.c 該函數在這里為swapinfo 結構分配指定大小的內存空間添加交換文件是通過函數swapadd來完成的,這個函數的流程如圖 7所示: 圖 8 swapadd函數流程圖 交換文件是用于交換的有固定長度的、規則的文件,交換設備相當于磁盤分區,交換設備和交換文件的作用一樣,設備掛靠到系統上時,系統就把設備當作一個文件來操作,添加交換文件可看作添加設備,以下的交換文件和交換設備都可統一看作交換文件。系統中所說的塊和頁其實是一樣的,都是頁的意思。 在函數的開始,先把交換設備的vnode轉化成文件系統通用的的vnode格式,這通過函數common_specvp實現。common_specvp把返回的vnode賦給cvp,cvp就表示了欲添加設備的vnode。 改變設備vnode的 標志位。先對設備加鎖,然后計算wasswap和vnode的v_flag,其中,wasswap表明設備是否是交換設備,計算完后開鎖。對互斥量swap_lock加鎖,調用VOP_OPEN打開欲添加的設備,如果成功,VOP_OPEN會為設備返回一個新的vnode;如果打開不成功,則恢復剛才改變的wasswap值。開鎖。獲取交換設備的屬性,如果設備的大小為零,或無法確定大小,則進行出錯處理。如果設備大小超過系統所能接受的范圍,則把它的大小限定為MAXOFF32_T(0x7fffffff),因為32位的操作系統不支持超過32位尋址的交換設備。判斷該交換設備是否可寫入。這通過VOP_SETATTR設置設備屬性來決定,如果成功設置,則設備可寫,否則進入出錯處理。判斷設備是否可進行頁I/O,如果不支持文件系統操作,則轉出錯處理。一般地,如果在根文件系統上進行交換,不要把和miniroot文件系統相應的交換塊放在空閑的交換列表中。因此,如果加入的設備作為根文件,則可用塊的起始地址要進行計算,具體前面多少塊不能用由klustsize(外部定義)決定。如果設備不是根文件,則起始地址由調用者指定,如果未指定,則從第二張頁面開始,因為第一張頁面存儲設備的標志。計算開始的偏移地址soff,如果大于設備的大小,則轉出錯處理。計算尾端的偏移地址eoff,如果大于設備大小,轉出錯處理。開始的偏移量soff和尾端的偏移量eoff進行頁面對齊,如果soff>=eoff,轉出錯處理。調用kmem_zalloc函數分配給swapinfo結構相應大小的內存。分配的空間的指針返回給nsip,則nsip指出交換設備的相關交換信息。對nsip的部分變量進行賦值:它的vnode,起始偏移量,尾端偏移量,對應的設備名稱。。。計算設備插槽(插槽相當于頁)相應位圖需要的字節數,給位圖分配相應的內存,并對每一位進行置位,然后檢查是否可以把交換設備添加到系統中。首先對swapinfo進行加鎖,由mutex_enter(&swapinfo_lock)完成。接著檢查全局變量swapinfo中是否已有該設備的vnode,如果找到的vnode和欲添加的vnode的偏移量一樣,只是先前被刪去的,把它恢復即可,然后解鎖,跳出程序;如果找到的vnode的偏移量的范圍被設備的偏移量完全覆蓋,則出錯。把設備加到列表中。判斷k_anoninfo中申請的頁數是否大于加上鎖的頁數,如小于,出錯;再判斷k_anoninfo中頁數的總量是否大于申請的數目,如果小于,出錯處理。把設備的頁數加到k_anoninfo的總數上,然后把cpu中相關線程的ani_count加上設備的頁數。如果在k_anoninfo中申請的頁面數大于已加鎖的,說明有一些申請為滿足,現在加入了一個設備,可把這個設備的頁面分配給請求者。如果系統中尚未有備份裝置,把剛添加的設備初始化為備份裝置。 1.3.3.5.刪除交換文件 表5 刪除交換文件時用到的主要函數 函數名 文件名 功能描述 swapdel uts/common/vm/Vm_swap.c 刪除某個設備刪除交換文件是通過函數swapdel這個函數的流程如圖 9所示: 圖 10swapdel函數流程圖 刪除交換文件是通過swapdel函數來完成的,調用者必須傳給swapdel欲刪除文件的vnode和刪除區域的起始塊地址。首先,函數把設備文件的vnode轉換成文件系統通用的vnode格式,這通過函數common_specvp實現。common_specvp把返回的vnode賦給cvp,cvp就表示了欲刪除設備的vnode。進行頁面對齊,獲取設備的開始偏移量soff。對全局變量swapinfo上鎖,在swapinfo中尋找設備的vnode。如果未找到,轉出錯處理。設備的信息賦給osip變量。對匿名內存k_anoninfo結構的信息進行判斷。如果申請的內存頁數小于被鎖的內存交換頁數,跳出程序;如果可申請的磁盤交換頁數小于已申請的磁盤交換頁數,跳出程序。如果系統中的所有的空閑頁數目小于欲刪除設備的頁數目,說明設備正在被使用,或系統無法騰出空間裝載在刪除設備中的內容,轉出錯處理。如果刪除設備后,請求的磁盤交換空間不足,可申請內存交換空間補足。先計算差額,然后從availrmem中分配內存。如果全局變量k_anoninfo中申請的內存交換空間小于被鎖住的,跳出程序;如果可分配的磁盤交換空間小于申請的數目,跳出程序;從系統中減去所刪除設備的頁數。對設備信息Osip的的標志位進行置位,防止再從該設備分配交換空間。準備釋放該設備的物理交換頁。在系統中,每個匿名頁都有一個anon結構。這個anon結構(slot)提供了匿名頁和其對應后備存儲的關系。對整個anon哈希表進行遍歷,找出有交換頁在欲刪除設備的anon slot,更新anon slot。在每個頁釋放后,都要返回anon slot相應的桶的開始,因為在釋放頁的時候,并沒有對整個哈希表加鎖,所以在釋放時哈希表可能會被別的進程改變。對哈希表一個個桶逐個遍歷。首先,獲取全局變量anon_hash的初始地址,然后對第一個桶進行分析,先對其加鎖。然后找這個桶中的每一個anon結構,如果某個anon的后備存儲為欲刪設備的頁,測試該頁對應的位圖情況,如果該頁已被使用,則把該頁對應vnode的v_count加1,把該頁從slot中釋放,然后把該頁對應的v_count減1。如果釋放成功,返回當前桶,繼續查找。如果釋放失敗,要把該頁恢復,全局變量k_anoninfo和availrmem等的相關值要重新加上該頁,然后轉出錯處理程序。遍歷完哈希表后,判斷是否完全完成釋放,這時應有空閑頁數和設備的頁數相等,否則終止程序。把設備從swapinfo列表中刪除。如果設備處于列表中的最后一個,則修改指針silast;釋放設備對應位圖的內存,釋放設備swapinfo對應的內存。如果設備屬于后備裝置,釋放。釋放設備的vnode,程序完成。 1.4.物理頁面管理 1.5.Vmem分配器 1.6.內核內存的初始化與布局 1.7.內核內存分配 1.7.1.概述 Solaris的內核內存分配器分成兩個層次。下層是后備分配器,它負責從內核地址空間分配整塊的內存供上層分配器使用,它分配的內存塊大小一般是一個頁或者是頁的整數倍。上層是slab分配器,它從后備分配器那里獲得整塊內存,然后把整塊內存分成小的內存塊,分配給內核程序。一整塊內存被稱為一個slab,這也是slab分配器名字的由來。后備分配器是一個vmem分配器,這是一個通用的資源分配器。后面的章節會對vmem分配器進行分析,這一節主要是分析slab分配器。 slab分配器吸收了面向對象的思想,它可以直接給用戶分配已經初始化好的對象,而且在釋放對象的時候,它也會調用相應的析構函數來銷毀對象。Slab分配器還采用了緩存的技術,每種類型的對象有一個單獨的緩存,以前分配的對象在釋放時并不是馬上銷毀,而是放回這個緩存中。以后再分配時,如果緩存中有空閑對象,就直接從緩存中分配,沒有的話再去創建一個新對象。采用緩存的辦法避免了每次分配對象都要進行一次初始化,從而加快了分配的速度。用戶在創建對象緩存的時候需要指定對象的規格,包括對象的尺寸、對齊邊界、構造函數和析構函數。除了可以分配初始化好的對象,slab分配器還實現了傳統的內存分配接口,可以分配任意大小的未初始化內存。在多CPU平臺中,不同的CPU在同時向內存分配器請求分配內存的時候會發生沖突。為了防止沖突破壞內存分配器的一致性,一個CPU在分配內存的時候必須對分配器加鎖,防止其他CPU同時分配內存。這樣的后果是不同CPU的內存分配操作必須順序進行,后一個CPU必須在前一個CPU分配完后才能開始分配,這就會造成效率的降低。CPU數量越多,這個問題越嚴重。為了解決這個問題,Solaris為每一個CPU配置了一個自己的局部緩存。CPU在分配內存的時候,首先從自己的緩存中分配,如果自己的緩存已空再從全局緩存中分配,這樣就降低了CPU沖突的概率。 CPU的局部緩存借用了自動步槍的原理,一個CPU是一支步槍,緩存中的對象是步槍的子彈,每分配一個對象就相當于打出一發子彈。子彈被放到彈夾(magazine)中,一個CPU的局部緩存中有兩個彈夾。分配對象時如果所有彈夾都被打空,就從全局分配器中換上一個滿彈夾。CPU釋放一個對象的時候相當于又獲得了一發子彈,它把這發子彈壓入彈夾中,供以后使用。釋放對象時如果CPU局部緩存中所有彈夾都是滿的,則用從全局分配器中換上一個空彈夾。為了存放全局的彈夾,slab分配器又引入了一個depot層,這個層相當于一個彈藥庫。depot層中有兩個鏈表,一個是空彈夾鏈表,一個是滿彈夾鏈表,每個CPU從彈藥庫中換彈夾時就是對這兩個鏈表進行操作。因此slab分配器可以看作是由三個層組成。最下層是slab層,它與頁分配器進行交互,申請或釋放整塊的內存,并把整塊內存劃分成小塊,供上層使用。中間是depot層,它是一個全局的彈夾管理器。最上面是CPU層,主要處理CPU的局部緩存。 Solaris中的slab分配器還考慮到了對CPU高速cache的影響。如果所有的對象都是從相同的對齊邊界開始(例如512字節對齊邊界),那么不同對象映射到同一個CPU緩存線(cache line)的概率就會增加,相應地也會增加cache的沖突和失效率,從而造成系統性能的下降。Solaris中引入了一個簡單的染色機制來解決這個問題。在對象緩存中,一個slab中對象的起始地址由一個染色值(color)決定,染色值不同,對象的起始地址也不同,所以映射到同一個緩存線的概率就會降低。一個對象緩存有一組可用的染色值,在創建slab的時候,這組染色值被循環使用,使得具有相同染色值的slab數量盡可能少。這樣就減少了cache沖突的次數,提高了系統的整體性能。 1.7.2.數據結構 1.7.2.1.重要數據結構間關系 圖14 數據結構關系圖 slab分配器中主要的實體是對象緩存。對象緩存的控制結構是kmem_cache,從kmem_cache出發可以訪問到緩存相關的數據結構,如kmem_slab、kmem_magazine等。系統中所有的對象緩存被串成一個雙向鏈表,通過全局變量kmem_null_cache可以訪問這個鏈表。一個對象緩存中可以包括多個slab,所有的slab鏈成一個雙向循環鏈表,通過kmem_cache中的cache_freelist字段可以引用這個鏈表。slab的控制結構是kmem_slab。一個slab被分成多個對象,每一個對象用一個kmem_bufctl結構來控制。slab中空閑的對象鏈成一個單向鏈表,kmem_slab中的slab_head字段指向這個鏈表的表頭。對象緩存中有兩個彈夾的鏈表,一個是空彈夾鏈表,一個是滿彈夾鏈表。彈夾的鏈表用kmem_maglist結構表示,鏈表中每一個彈夾用kmem_magazine結構表示。一個彈夾中可以裝載多個對象,對每一個裝載的對象,kmem_magazine中保存一個指向該對象起始地址的指針。每一個CPU有自己的局部緩存,這個局部緩存由kmem_cpu_cache結構控制。kmem_cache結構中有一個kmem_cpu_cache結構的數組,其中每一個元素表示一個CPU的局部緩存。每個CPU的局部緩存中包含兩個彈夾,一個是當前裝載的彈夾,一個是前一個裝載的彈夾。 1.7.2.2.kmem_cache kmem_cache結構是對象緩存的控制結構,每一個對象緩存都對應一個kmem_cache結構的變量。kmem_cache結構在uts/common/sys/kmem_impl.h中定義,它的定義如下: struct kmem_cache { /* 以下變量用于統計 */ uint64_t cache_slab_create; /* slab創建的次數 */ uint64_t cache_slab_destroy; /* slab銷毀的次數 */ uint64_t cache_slab_alloc; /* slab層分配的次數 */ uint64_t cache_slab_free; /* slab層釋放的次數 */ uint64_t cache_alloc_fail; /* 分配失敗的總次數 */ uint64_t cache_buftotal; /* 總的對象個數 */ uint64_t cache_bufmax; /* 出現過的最大對象個數 */ uint64_t cache_rescale; /* 重新調整hash表的次數 */ uint64_t cache_lookup_depth; /* hash查找的深度 */ uint64_t cache_depot_contention; /* depot層互斥沖突的次數 */ uint64_t cache_depot_contention_prev; /* depot層互斥沖突次數的前一個快照 */ /* 對象緩存的屬性 */ char cache_name[KMEM_CACHE_NAMELEN + 1]; /* 緩存名稱 */ size_t cache_bufsize; /* 對象大小 */ size_t cache_align; /* 對象的對齊邊界 */ int (*cache_constructor)(void *, void *, int); /* 構造函數 */ void (*cache_destructor)(void *, void *); /* 析構函數 */ void (*cache_reclaim)(void *); /* 回收函數 */ void *cache_private; /* 構造函數、析構函數和回收函數的參數 */ vmem_t *cache_arena; /* slab的后備分配器 */ int cache_cflags; /* 緩存的創建標識 */ int cache_flags; /* 緩存的狀態信息 */ uint32_t cache_mtbf; /* induced alloc failure rate */ uint32_t cache_pad1; /* to align cache_lock */ kstat_t *cache_kstat; /* exported statistics */ kmem_cache_t *cache_next; /* 系統中后一個緩存 */ kmem_cache_t *cache_prev; /* 系統中前一個緩存 */ /* Slab層 */ kmutex_t cache_lock; /* 保護slab層的互斥鎖 */ size_t cache_chunksize; /* 對象在slab中的實際大小 */ size_t cache_slabsize; /* 一個slab的大小 */ size_t cache_bufctl; /* buf起始地址到kmem_bufctl的偏移 */ size_t cache_buftag; /* buf起始地址到kmem_buftag的偏移 */ size_t cache_verify; /* 需要驗證的字節數 */ size_t cache_contents; /* bytes of saved content */ size_t cache_color; /* 創建下一個slab時用的染色值 */ size_t cache_mincolor; /* 可用的最小染色值 */ size_t cache_maxcolor; /* 可用的最大染色值 */ size_t cache_hash_shift; /* hash運算時地址移位的位數 */ size_t cache_hash_mask; /* hash表的掩碼 */ kmem_slab_t *cache_freelist; /* 空閑slab鏈表 */ kmem_slab_t cache_nullslab; /* 空閑鏈表尾部的標記 */ kmem_cache_t *cache_bufctl_cache; /* 分配kmem_bufctls結構的緩存 */ kmem_bufctl_t **cache_hash_table; /* hash表的基地址 */ void *cache_pad2; /* to align depot_lock */ /* Depot層 */ kmutex_t cache_depot_lock; /* 保護depot層的互斥鎖 */ kmem_magtype_t *cache_magtype; /* 彈夾類型 */ void *cache_pad3; /* to align cache_cpu */ kmem_maglist_t cache_full; /* 滿彈夾列表 */ kmem_maglist_t cache_empty; /* 空彈夾列表 */ /* CPU層 */ kmem_cpu_cache_t cache_cpu[1]; /* CPU局部緩存控制結構數組 */ }; kmem_cache中的字段分成五個部分。第一個部分是用于統計的字段,系統每次執行與該緩存相關的操作時會更新這些字段,它們的值用于做系統負載、性能等方面的統計。第二個部分是表示緩存屬性的字段。 cache_bufsize和cache_align保存了緩存中對象的大小和對齊邊界。 cache_constructor、cache_destructor和cache_reclaim是回調函數,它們分別是構造函數、析構函數和回收函數,這些函數由用戶在創建緩存的時候指定。cache_private是調用這三個回調函數時用到的參數。 cache_arena是比緩存低一級的分配器,緩存從這里獲取整塊內存(一般是頁的整數倍)來創建slab,然后再劃分成對象,釋放slab時slab對應的內存也歸還到這里。系統中所有的緩存通過cache_next和cache_prev字段鏈成一個雙向循環鏈表,這個鏈表的表頭是kmem_null_cache,這是一個kmem_cache類型的全局變量,定義在uts/common/os/kmem.c中。第三個部分是與slab層相關的字段。 cache_chunksize表示一個對象在slab中的實際大小。一個對象在slab中除了它本身要占用空間外,為了對齊還要占用一些空間,另外可能還會有一些調試信息,所有這些空間合稱為一個chunk,而cache_chunksize就是它的大小。 cache_slabsize是緩存中一個slab的大小,這個大小與cache_chunksize有關。一般小對象(chunksize小于512字節)對應的slab大小都是一個整頁(4K字節)。在slab中,如果一個小對象是空閑的,而且這個對象的大小可以放下kmem_bufctl和kmem_buftag結構,那么這個對象對應的內存塊會被用來存放這兩個控制結構。kmem_bufctl和kmem_buftag結構位于內存塊的末尾,cache_bufctl和cache_buftag字段分別表示對象的起始地址到這兩個結構的起始地址之間的距離。如果對象太大,使得一個slab可能跨越多個頁,或者對象太小,不能放下kmem_bufctl結構,那么即便對象是空閑的,kmem_bufctl也不能放在對象所占的內存塊中,而是要另外為kmem_bufctl分配一塊內存。在為kmem_bufctl分配內存的時候,把kmem_bufctl也看作是小對象,用的是與其它對象相同的分配方法。cache_bufctl_cache字段就是指向用于分配kmem_bufctl結構的對象緩存。 cache_hash_table是一個kmem_bufctl結構的hash表。當kmem_bufctl結構不位于對象所占的內存塊中時,就不能通過對象的起始地址計算出kmem_bufctl的地址。為了能快速找到對象的kmem_bufctl,Solaris為kmem_bufctl建了一個hash表,表的索引值就是對象的起始地址,這樣就可以通過起始地址快速在表中它的kmem_bufctl。 kmem_hash_shift和kmem_hash_mask字段與hash函數有關。slab中hash函數的過程是先把kmem_bufctl的地址右移kmem_hash_shift位,然后再與kmem_hash_mask做與操作,得到的值就是kmem_bufctl結構在hash表中的索引。 cache_color、cache_mincolor和cache_maxcolor這三個字段用于對slab進行染色。cache_color是創建下一個slab時用到的染色值,cache_mincolor是可用的最小染色值,cache_maxcolor是可用的最大染色值。一個對象緩存中所有的slab串成一個雙向循環鏈表,kmem_nullslab標識這個鏈表的結尾。這個鏈表分成三個部分,所有對象都已被分配的slab位于最前面的部分,部分被分配的slab位于中間,而完全空閑的slab位于尾部。部分被分配的部分和完全空閑的部分合稱為空閑列表,cache_freelist指向這個鏈表的表頭。kmem_nullslab是整個鏈表的尾部,因此它也是空閑鏈表的尾部。第四個部分是depot層相關的字段。 cache_magtype表示彈夾的類型。彈夾分成不同的類型,不同類型的彈夾中可以存放的對象數各不相同,可裝載對象的尺寸范圍也不同。這個部分中還有兩個鏈表字段,一個是滿彈夾鏈表cache_full,一個是空彈夾鏈表cache_empty,這兩個鏈表中存放的是全局的彈夾。第五個部分是與CPU層相關的字段。這個部分中只有一個字段cache_cpu,這是一個kmem_cpu_cache結構的數組,數組中有多少個元素由CPU的個數決定。每一個元素對應一個CPU,表示該CPU的局部緩存。在kmem_cache結構的聲明中,這個數組只有一個元素,但是在創建緩存的時候,系統會根據CPU的數目給kmem_cache結構多分配一些內存,保證數組中可以放下所有CPU局部緩存的控制結構。 1.7.2.3.kmem_slab kmem_slab結構是slab的控制結構,它在uts/common/sys/kmem_impl.h中定義,定義如下: typedef struct kmem_slab { struct kmem_cache *slab_cache; /* 所屬對象緩存的控制結構指針 */ void *slab_base; /* 第一個對象的起始地址 */ struct kmem_slab *slab_next; /* 空閑鏈表中的下一個slab */ struct kmem_slab *slab_prev; /* 空閑鏈表中的上一個slab */ struct kmem_bufctl *slab_head; /* 空閑對象鏈表的表頭 */ long slab_refcnt; /* 已經分配的對象數 */ long slab_chunks; /* 共有多少個對象 */ } kmem_slab_t; slab_cache字段是一個指向這個slab所屬對象緩存控制結構的指針。 slab_base字段表示這個slab中第一個對象的起始地址。前面提到,slab的第一個對象不一定位于slab的起始位置,它的地址與創建slab時分配的染色值有關。slab_base保存了第一個對象的地址,在實現中它就等于slab的起始地址加上染色值。 slab_next和slab_prev用來構建空閑slab鏈表。一個slab中所有空閑對象的控制結構組成一個單向鏈表,slab_head就指向這個鏈表的表頭。 slab_refcnt是引用數,實際上就表示這個slab中已經有多少個對象已經被分配出去。 slab_chunks表示slab中包括多少個chunk。一個chunk對應一個對象,因此這個字段實際上也指出了slab中有多少個對象。 1.7.2.4.kmem_bufctl kmem_bufctl是對象所占內存塊的控制結構,它在uts/common/sys/kmem_impl.h中定義,定義如下: typedef struct kmem_bufctl { struct kmem_bufctl *bc_next; /* 空閑列表中下一個內存塊 */ void *bc_addr; /* 內存塊的起始地址 */ struct kmem_slab *bc_slab; /* 所屬slab的控制結構指針 */ } kmem_bufctl_t; 這個結構中只有三個字段。bc_next表示空閑列表中的下一個內存塊;bc_addr表示這個內存塊的起始地址,實際上也是對象的地址;bc_slab表示這個內存塊所屬slab的控制結構的指針。 1.7.2.5.kmem_magazine、kmem_maglist和kmem_magtype 這三個數據結構是在管理彈夾的時候用到的主要數據結構,它們都是在uts/common/sys/kmem_impl.h中定義,定義如下: typedef struct kmem_magazine { void *mag_next; /* 彈夾鏈表中的下一個彈夾 */ void *mag_round[1]; /* 一發或多發子彈 */ } kmem_magazine_t; /* 每個CPU進行分配時所用的彈夾類型 */ typedef struct kmem_magtype { int mt_magsize; /* 彈夾尺寸(有多少發子彈) */ int mt_align; /* 彈夾對齊邊界 */ size_t mt_minbuf; /* 適用的最小內存塊尺寸 */ size_t mt_maxbuf; /* 允許調整的最大內存塊尺寸 */ kmem_cache_t *mt_cache; /* 用于分配彈夾數據結構的對象緩存 */ } kmem_magtype_t; /* 用于depot層的彈夾列表 */ typedef struct kmem_maglist { kmem_magazine_t *ml_list; /* 彈夾鏈表 */ long ml_total; /* 總彈夾數 */ long ml_min; /* 上次更新后彈夾數的最小值 */ long ml_reaplimit; /* 最多可以釋放的彈夾數 */ uint64_t ml_alloc; /* 從這個鏈表中分配了多少個彈夾 */ } kmem_maglist_t; kmem_magazine是彈夾的控制結構,它有兩個字段。mag_next字段指向彈夾鏈表中的下一個彈夾。mag_round是一個數組,其中每一個位置可以裝一發子彈。如果一個位置裝了一發子彈,那么這個位置的指針就指向一個空閑對象。在聲明中這個數組只有一個元素,但在實際分配內存的時候,系統會根據彈夾中能裝的子彈數分配出足夠的內存。 kmem_magtype表示彈夾的類型。 mt_magsize表示彈夾中可以裝多少發子彈,kmem_magazine中mag_round數組的大小就是由這個字段來決定。 mt_align表示彈夾的對齊邊界。 mt_minbuf和mt_maxbuf限定了這個彈夾類型的適用范圍。一種類型的彈夾并不是可以用于所有的對象,它所能裝對象的尺寸是有一定限制的,這兩個字段就給出了尺寸范圍。這兩個字段表示的尺寸不是對象本身的大小,而是前面所說的chunksize,也就是對象加上對齊和調試信息之后實際所占的內存塊大小。一個彈夾中裝載的對象的chunksize必須大于mt_minbuf。mt_maxbuf的用法與mt_minbuf的用法有所不同,它用于調整彈夾的類型。一個CPU在運行的時候可以調整它所用的彈夾類型,如果它發現當前類型中的彈夾裝載的子彈數太少,就會選擇子彈數更多的彈夾類型。在調整彈夾的時候,需要判斷對象的chunksize是否比當前彈夾類型的mt_maxbuf小。如果chunksize小于mt_maxbuf,則允許調整,CPU可以選擇下一級別的彈夾類型,否則的話就不能進行彈夾的調整。 mt_cache表示分配這種類型的彈夾使用哪一個對象緩存,分配不同類型的彈夾用到的對象緩存可能是不同的,但是分配同一類型的彈夾用的都是同一個對象緩存。系統中共定義了九種彈夾類型,每個彈夾必定屬于其中一種類型。這九種類型在uts/common/os/kmem.c中定義,定義如下: static kmem_magtype_t kmem_magtype[] = { { 1, 8, 3200, 65536}, { 3, 16, 256, 32768}, { 7, 32, 64, 16384}, { 15, 64, 0, 8192 }, { 31, 64, 0, 4096 }, { 47, 64, 0, 2048 }, { 63, 64, 0, 1024 }, { 95, 64, 0, 512 }, { 143, 64, 0, 0 }, }; 在這個定義中,只給出了每種類型前四個字段的值,最后一個字段,也就是mt_cache字段的值沒有給出。mt_cache字段指向分配彈夾所用的對象緩存,這些對象緩存是在uts/common/os/kmem.c中的kmem_cache_init函數中創建的,所以mt_cache字段也是在kmem_cache_init函數中賦的值。 kmem_maglist表示一個彈夾鏈表。 ml_list是鏈表的表頭,ml_total是鏈表中的總彈夾數。 ml_min表示從上次更新到當前時間彈夾鏈表中出現的彈夾數最小值。Solaris會定期更新對象緩存,也就是做對象緩存維護,包括更新對象緩存的統計信息、更新depot層的工作集和調整彈夾大小。每次在更新depot層工作集的時候,它會把depot層的空彈夾鏈表和滿彈夾鏈表中的ml_min設成鏈表的當前總彈夾數。隨著對象的分配和回收,彈夾鏈表中的彈夾數也在不斷變化,而ml_min保存了自上次更新以后出現過的最小彈夾數。 ml_reaplimit用于回收depot層的彈夾。這里有一個彈夾工作集的概念。在系統穩定工作的時候,所需的內存會保持在一個穩定的值,因此depot層中所需的彈夾數也會在一個穩定的范圍內,這時,所需的彈夾就構成一個彈夾工作集。例如在一段時間內,一個彈夾鏈表的彈夾數在30到40之間浮動,那么彈夾工作集就是10個彈夾(40 - 30),其余的彈夾一直閑置在彈夾鏈表中,在回收的時候就要回收這些閑置的彈夾。實際上ml_min就是閑置的彈夾數,因為它表示了一段時間內不同彈夾的最小值,在更新depot工作集的時候,Solaris會把ml_min的值賦給ml_reaplimit。在下次更新depot工作集之前,ml_reaplimit的值就不會再改變了,但是彈夾鏈表中的彈夾數是會發生變化的,所以在實際回收的時候,回收的彈夾數會取ml_reaplimit和當前彈夾總數中的最小值。 1.7.2.6.kmem_cpu_cache kmem_cpu_cache結構是CPU局部緩存的控制結構,它在uts/common/sys/kmem_impl.h中定義,定義如下: typedef struct kmem_cpu_cache { kmutex_t cc_lock; /* 保護CPU局部緩存的互斥鎖 */ uint64_t cc_alloc; /* 這個局部緩存中已分配的對象數 */ uint64_t cc_free; /* 這個局部緩存中的空閑對象數 */ kmem_magazine_t *cc_loaded; /* 當前裝載的彈夾 */ kmem_magazine_t *cc_ploaded; /* 前一個裝載的彈夾 */ int cc_rounds; /* 當前裝載的彈夾中還有多少發子彈 */ int cc_prounds; /* 前一個裝載的彈夾中還有多少發子彈 */ int cc_magsize; /* 滿彈夾中有多少發子彈 */ int cc_flags; /* 全局對象緩存cache_flags字段的拷貝 */ char cc_pad[KMEM_CPU_PAD]; /* 用于邊界對齊 */ } kmem_cpu_cache_t; cc_alloc表示當前CPU的局部緩存中已經分配了多少個對象,cc_free表示還有多少個空閑對象。 cc_loaded和cc_ploaded表示CPU裝載的兩個彈夾。當從depot層裝載一個新彈夾的時候,cc_loaded指向新裝載的彈夾,cc_ploaded指向cc_loaded原來所指的彈夾,而cc_ploaded原來所指的彈夾被歸還到depot層。保存兩個彈夾的目的是為了防止抖動。如果只有一個彈夾,現在考慮彈夾中只有一個對象的情況,如果CPU要連續分配兩個對象,那么第一個對象可以直接從彈夾中分配,而分配第二個對象時就需要局部緩存從depot層裝載一個滿彈夾,并把當前的空彈夾還給depot層,再從滿彈夾中分配;下一步CPU要連續釋放兩個對象,由于彈夾中還有一個空位,所以第一個對象可以直接存到彈夾中,但是釋放第二個對象時就需要從depot層裝載一個空彈夾,并把滿彈夾還給depot層,然后把空閑對象放到空彈夾中。這時,CPU的局部緩存中的彈夾又變成只有一個對象的情況。如果這種操作反復進行,那么局部緩存就需要反復地與depot層交互,從而出現抖動。如果有兩個彈夾的話,只要其中有一個彈夾中有空閑對象,就可以分配對象,而只要其中一個彈夾有空位,就可以釋放對象,這就避免了抖動的情況。 cc_rounds和cc_prounds分別表示當前裝載的彈夾和前一個裝載的彈夾中的空閑對象數,而cc_magsize表示彈夾中最多可以裝載多少個對象。 cc_flags是局部緩存所在的對象緩存中cache_flags字段的拷貝,在這里放一份拷貝是為了操作方便。 1.7.3.情景 1.7.3.1.創建對象緩存 表9 創建對象緩存時用到的主要函數函數名 文件名 功能描述 kmem_cache_create uts/common/os/kmem.c 在內核空間分配一個已初始化的對象創建對象緩存是通過kmem_cache_create函數完成的,這個函數的流程如圖2所示: 圖15 kmem_cache_create函數流程圖創建對象緩存的時候需要給對象緩存起一個名字,這個名字要符合C語言的命名規范,kmem_cache_create函數首先會調用strident_valid函數來驗證緩存的名字。然后要為對象緩存的控制結構分配空間,這通過調用vmem_xalloc函數來完成。vmem_xalloc函數從kmem_cache_arena區域分配一塊內存,內存的大小通過KMEM_CACHE_SIZE宏來計算。這個宏根據系統中CPU的個數調整內存塊的大小,保證位于控制結構末尾的局部緩存數組可以容下所有CPU的局部緩存。新分配的內存塊中所有的字節都被初始化為0。接下來就要設置控制結構的各個字段了,這大致分成設置對象緩存屬性、設置slab層、設置depot層和設置CPU層幾個部分,下面就部分字段進行說明。 cache_align字段根據align參數來設置。首先要檢查align的值,如果它的值為0,就把它設成默認的對齊邊界(8字節,在KMEM_ALIGN宏中定義)。另外align的值必須是2的冪,而且它不能大于下層分配器的基本分配單位,否則會報錯。接下來kmem_cache_create函數會對cflags進行檢驗和設置,保證里面的各個標識位都是合理的,然后把cflags的值賦給cache_cflags字段。 cache_constructor、cache_destructor等字段由調用者通過參數來設置。 cache_chunksize字段的值需要根據cache_bufsize字段的值來計算。首先把cache_bufsize按對齊邊界對齊;如果cache_flags中的KMF_BUFTAG位被置位,就再加上kmem_buftag結構的大小;最后再按對齊邊界對齊一次,得到的值就是chunk的大小。有了chunk的大小,就可以算出cache_bufctl、cache_buftag、cache_contents等字段的值。然后要找一個合適的slab大小。如果cache_chunksize小于下層分配器基本分配單位的1/KMEM_VOID_FRACTION(KMEM_VOID_FRACTION的值為8),而且cache_cflags的 KMF_NOHASH位被置位,則slab的大小就等于基本分配單位的大小。否則的話就分別考慮slab中分別包括1個到8個chunk的情況,從中找出內存浪費最少的一個情況,slab的大小就等于這種情況下的chunk數乘以chunk大小,再按基本分配單位對齊。 depot層的設置主要就是考慮選擇一個合適的彈夾類型,選擇的原則就是從彈夾類型鏈表中找第一個適用的彈夾類型(mt_minbuf字段小于chunk大小的類型),找到后就把這個類型賦給cache_magtype字段。設置完各個字段的值后,kmem_cache_create函數會把新建的對象緩存添加到全局對象緩存鏈表中,把它插在kmem_null_cache之前。最后,如果kmem_ready的值非0,也就是內核內存的初始化已經完成,說明用于分配彈夾的對象緩存已經創建完畢,這時就開啟對象緩存的彈夾機制。 1.7.3.2.分配對象 這個情景描述在內核空間分配一個已經初始化好的對象,其中涉及到的主要函數包括:表10 內核對象分配中的主要函數函數名 文件名 功能描述 kmem_cache_alloc uts/common/os/kmem.c 在內核空間分配一個已初始化的對象 kmem_depot_alloc uts/common/os/kmem.c 從depot層申請一個滿彈夾 kmem_depot_free uts/common/os/kmem.c 把CPU換下的空彈夾加入到depot層的空彈夾鏈表中 kmem_slab_alloc uts/common/os/kmem.c 在slab層創建一個對象,并把它分配給調用者 kmem_slab_create uts/common/os/kmem.c 為一個對象緩存創建一個新的slab 內核程序要分配一個對象的時候,需要調用kmem_cache_alloc函數。這個函數的流程如圖2所示: 圖16 kmem_cache_alloc函數流程圖 kmem_cache_alloc首先從CPU自己的緩存中分配。如果CPU的兩個彈夾中都沒有可分配的對象,則調用kmem_depot_alloc函數從depot層的滿彈夾鏈表中裝載一個滿彈夾,然后把滿彈夾設為當前彈夾,重新執行分配過程。裝載滿彈夾時會替換下CPU前一個裝載的彈夾,被換下的彈夾一定是一個空彈夾,所以要調用kmem_depot_free函數把這個彈夾放入depot層的空彈夾鏈表中。如果獲取滿彈夾失敗,則說明滿彈夾列表為空,depot層也沒有可以分配的對象,這時就調用kmem_slab_alloc函數直接從slab層分配一個對象。kmem_slab_alloc分配的對象是未初始化的對象,最后還要用構造函數對這個對象進行初始化。從slab層分配對象時調用的是kmem_slab_alloc函數,這個函數的流程如圖3所示: 圖17 kmem_slab_alloc函數流程圖 這個函數中需要說明的是如何獲取slab中一個空閑內存塊的地址。獲取地址的代碼如下: if (cp->cache_flags & KMF_HASH) { /* * Add buffer to allocated-address hash table. */ buf = bcp->bc_addr; hash_bucket = KMEM_HASH(cp, buf); bcp->bc_next = *hash_bucket; *hash_bucket = bcp; if ((cp->cache_flags & (KMF_AUDIT | KMF_BUFTAG)) == KMF_AUDIT) { KMEM_AUDIT(kmem_transaction_log, cp, bcp); } } else { buf = KMEM_BUF(cp, bcp); } 從kmem_slab結構中只能獲取空閑內存塊的控制結構,也就是一個kmem_bufctl變量,然后需要從kmem_bufctl得出空閑塊的地址。在kmem_cache結構的介紹中提到內存塊的控制結構可能放在內存塊中,也可能另外為它分一塊內存,如果另外分配內存的話,就需要一個存放控制結構的hash表。kmem_cache結構的cache_flag字段中有一位會標出是否要用到hash表。在從kmem_bufctl結構獲得內存塊地址的時候,要對這一位進行判斷。如果用到hash表,則kmem_bufctl的bc_addr字段就指向內存塊的地址;如果kmem_bufctl就在內存塊中,就可以從控制結構直接計算出內存塊的地址。這個計算是通過KMEM_BUF宏完成的,這個宏在uts/common/sys/kmem_impl.h中定義。在沒有空閑slab時需要調用kmem_slab_create函數創建一個新的slab,它的流程如圖4所示: 圖18 kmem_slab_create函數流程 這個函數在為kmem_slab控制結構分配內存的時候,會根據對象緩存是否使用hash表來決定是為控制結構另外分配一塊內存還是利用slab末尾的內存。同樣,在為每個小內存塊的控制結構分配內存的時候,也要根據是否使用hash表來決定是為控制結構另外分配內存還是直接利用小內存塊本身的內存。如果不使用hash表,則不需要另外分配內存。為了節省空間,在流程圖中為小內存塊設置控制結構這一塊簡略表示了。為slab控制結構分配內存的代碼如下: if (cache_flags & KMF_HASH) { if ((sp = kmem_cache_alloc(kmem_slab_cache, kmflag)) == NULL) goto slab_alloc_failure; chunks = (slabsize - color) / chunksize; } else { sp = KMEM_SLAB(cp, slab); chunks = (slabsize - sizeof (kmem_slab_t) - color) / chunksize; } 代碼中為控制結構另外分配內存時調用的也是kmem_cache_alloc函數,它從kmem_slab對應的對象緩存中分配。kmem_slab的對象緩存是不使用hash表的,否則在分配kmem_slab對象的時候有可能引起對kmem_slab對象緩存的遞歸分配,最后導致內存分配失敗。 KMEM_SLAB宏用來根據slab的地址計算slab控制結構的地址。相應地,在分配小內存塊控制結構的時候,KMEM_BUFCTL宏用來根據小內存塊的地址計算小內存塊控制結構的地址。 1.7.3.3.釋放對象 表11 釋放對象時用到的主要函數函數名 文件名 功能描述 kmem_cache_free uts/common/os/kmem.c 釋放一個已初始化的對象 kmem_slab_free uts/common/os/kmem.c 在slab層釋放一個內存塊 kmem_slab_destroy uts/common/os/kmem.c 銷毀一個slab 釋放對象是與分配對象相反的過程。釋放對象時調用的是kmem_cache_free,這個函數在uts/common/os/kmem.c中定義,它的流程如圖5所示: 圖19 kmem_alloc_free函數流程圖如果CPU當前裝載的彈夾中有空位,那么就把被釋放的對象放到空位中,也就是讓對象數組中的一個空閑指針指向被釋放的對象。如果當前裝載的彈夾是滿的,那么就看前一個裝載的彈夾是否還有空位。如果有空位的話就把當前裝載的彈夾和前一個裝載的彈夾交換一下位置,再重新做釋放操作。如果局部緩存中的兩個彈夾都是滿的,那么就需要調用kmem_depot_alloc函數從depot層獲取一個空彈夾,并通過調用kmem_depot_free函數把一個滿彈夾還給depot層,然后再重新做釋放操作。如果depot層的空彈夾鏈表中已經沒有空彈夾,就調用kmem_cache_alloc函數從彈夾的對象緩存中分配一個新彈夾(空彈夾),然后重新做釋放操作。這也就是說一個對象緩存在剛創建的時候,depot層中的滿彈夾鏈表和空彈夾鏈表都是空的,以后用到的彈夾都是在釋放對象時創建的。彈夾在被創建后,除非出現內存緊張或者需要調整彈夾尺寸的情況,它們不會被銷毀,而是隨著申請對象和釋放對象操作的交替進行,慢慢分布到depot層的空彈夾鏈表或滿彈夾鏈表中。如果創建新彈夾失敗,就無法把被釋放的對象緩存到depot層,這時就調用析構函數銷毀對象,再調用kmem_slab_free函數把對象所占的內存塊還給slab層。 kmem_slab_free函數的功能是釋放一個已分配的未初始化內存塊,它在uts/common/os/kmem.c中定義,它的流程如圖6所示: 圖20 kmem_slab_free函數流程圖 kmem_slab_free首先要得到內存塊的控制結構和slab的控制結構。如果這個slab使用hash表存儲控制結構,則查詢hash表,否則直接從內存塊的地址算出控制結構的地址。由于這個slab釋放了一個內存塊,也就是說這個slab現在肯定包含有空閑內存塊,所以如果這個slab以前不在對象緩存的空閑slab鏈表中,現在就把它加進去。然后把被釋放的內存塊插入到這個slab的空閑內存塊鏈表的頭部。如果釋放完這個內存塊后slab中所有的內存塊都是空閑的,就可以回收這個slab所占的內存空間。回收slab時首先要把這個slab從空閑slab鏈表中移除,如果這個slab恰好是空閑鏈表的表頭,那么還要修改表頭,讓表頭指向下一個空閑slab。然后就可以調用kmem_slab_destroy函數銷毀slab了。 kmem_slab_destroy函數比較簡單,它完成的工作包括兩步:第一步,如果slab的控制結構占用了另外的空間,就釋放這些空間;第二步,調用底層分配器的釋放函數(vmem_free),釋放slab所占的內存。 1.7.3.4.分配與釋放未初始化內存塊 表12分配與釋放未初始化內存塊時用到的主要函數函數名 文件名 功能描述 kmem_alloc uts/common/os/kmem.c 分配一塊任意大小的未初始化內存 kmem_zalloc uts/common/os/kmem.c 分配一塊任意大小的內存塊,內存塊全部初始化為0 kmem_free uts/common/os/kmem.c 釋放一個未初始化內存塊 Solaris的slab內存分配器除了可以分配已初始化的對象,它還可以像普通的分配器一樣分配和釋放任意大小的未初始化的內存,完成這兩個操作的函數分別是kmem_alloc和kmem_free。 kmem_alloc函數在uts/common/os/kmem.c中定義。在調用kmem_alloc函數的時候需要給出所需內存塊的尺寸,kmem_alloc會根據尺寸進行不同的分配操作。對于小于16k(在常量KMEM_MAXBUF中定義)的內存塊,slab分配器采用了與分配對象相同的方法,它把未初始化的內存看作構造函數和析構函數都為空的對象。系統在初始化的時候創建了32個用于分配未初始化內存的對象緩存,它們對應的內存塊尺寸在kmem_alloc_sizes數組中給出。 static const int kmem_alloc_sizes[] = { 1 * 8, 2 * 8, 3 * 8, 4 * 8, 5 * 8, 6 * 8, 7 * 8, 4 * 16, 5 * 16, 6 * 16, 7 * 16, 4 * 32, 5 * 32, 6 * 32, 7 * 32, 4 * 64, 5 * 64, 6 * 64, 7 * 64, 4 * 128, 5 * 128, 6 * 128, 7 * 128, P2ALIGN(8192 / 7, 64), P2ALIGN(8192 / 6, 64), P2ALIGN(8192 / 5, 64), P2ALIGN(8192 / 4, 64), P2ALIGN(8192 / 3, 64), P2ALIGN(8192 / 2, 64), P2ALIGN(8192 / 1, 64), 4096 * 3, 8192 * 2, }; 為了實現內存塊尺寸到32個對象緩存的映射,Solaris聲明了一個全局數組kmem_alloc_table。數組中有2048個元素(KMEM_MAXBUF >> KMEM_ALIGH_SHIFT),每個元素是一個指向對象緩存的指針。數組中每個元素對應一個內存塊尺寸,尺寸從前往后依次遞增,每次遞增的值是8字節(也就是最小對齊邊界,在常量KMEM_ALIGN中定義)。第一個元素對應8字節的內存塊,第二個元素對應16字節的內存塊,……,最后一個元素對應16k字節的內存塊。在初始化的時候,系統令kmem_alloc_table數組中某一元素指向大于它所對應內存塊尺寸的第一個對象緩存。kmem_alloc_table數組元素和32個對象緩存的對應關系如圖5所示。 圖21 kmem_alloc_talbe數組與32個對象緩存之間的關系如果要分配一定尺寸的一塊內存,分配器首先在kmem_alloc_table數組中找到大于該尺寸的第一個元素,再根據該元素中的指針找到相應的對象緩存,然后就調用前面所講的kmem_cache_alloc函數從對象緩存中分配一塊內存。如果要分配大于16k字節的內存塊,就不能用上面提到的這套機制了,這時solaris就調用vmem_alloc函數直接從kmem_oversize_arena區域分配所需的內存。 kmem_free函數的功能是釋放未初始化內存塊,它的原理與kmem_alloc差不多,只不過它執行了一個相反的過程。kmem_free函數也是根據內存塊的大小執行不同的釋放操作,如果內存塊的尺寸小于16k,它就調用kmem_cache_free函數釋放內存塊,否則它就直接調用vmem_free函數把內存塊歸還到kmem_oversize_arena區域中去。
總結
以上是生活随笔為你收集整理的转一个solaris虚拟内存管理的wiki的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 计算器 eval ctf_
- 下一篇: pb 日期相关函数