前面幾篇介紹了進程的一些知識,從這篇開始介紹內存、文件、IO等知識,發現更不好寫哈哈。但還是有必要記錄下自己的所學所思。供后續翻閱,同時寫作也是一個鞏固的過程。
這些知識以前有文檔涉及過,但是角度不同,這個系列站的角度更底層,基本都是從Linux內核出發,會更深入。所以當你都讀完,然后再次審視這些功能的實現和設計時,我相信你會有種豁然開朗的感覺。
1、頁
內核把物理頁作為內存管理的基本單元。
盡管處理器的最小處理單位是字(或者字節),但是MMU(內存管理單元,管理內存並把虛擬地址轉換為物理地址的硬件)通常以頁為單位進行處理。所以從虛擬內存看,頁也是最小單元。
體系不同,支持的頁大小不同。大多數32位體系結構支持4KB的頁,而64位體系結構一般會支持8KB的頁。
內核用struct page結構體表示系統中的每個頁,包含很多項比如頁的狀態(有沒有臟,有沒有被鎖定)、引用計數(-1表示沒有使用)等等。
page結構和物理頁相關,和虛擬內存無關。所以它的描述是短暫的,僅僅記錄當前的使用狀況,當然也不會描述其中的數據。
內核用這個結構來管理系統中所有的頁,所以內核知道哪些頁是空閑的,如果在使用中擁有者又是誰。
這個擁有者有四種:用戶空間進程、動態分配內存的內核數據、靜態內核代碼以及頁高速緩存。
2、區
有些頁是有特定用途的。比如內存中有些頁是專門用於DMA的。
內核使用區的概念將具有相似特性的頁進行分組。區是一種邏輯上的分組的概念,而沒有物理上的意義。
區的實際使用和分布是與體系結構相關的。在x86體系結構中主要分為3個區:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
ZONE_DMA區中的頁用來進行DMA(直接內存訪問)時使用。
ZONE_HIGHMEM是高端內存,其中的頁不能永久的映射到內核地址空間,也就是說,沒有虛擬地址。
剩余的內存就屬於ZONE_NORMAL區,叫低端內存。
不是所有體系都定義全部區,有些體系結構,比如x86-64可以映射和處理64位的內存空間,所以它沒有ZONE_HIGHMEM區,所有的物理內存都都處於ZONE_DMA和ZONE_NORMAL區。
每個區都用結構體struct zone表示。
3、接口
獲得頁
獲得頁使用的接口是alloc_pages函數與__get_free_page函數。后者也是調用了前者,只不過在獲得了struct page結構體后使用page_address函數獲得了虛擬地址。
我們在使用這些接口獲取頁的時候可能會面對一個問題,我們獲得的這些頁若是給用戶態用,雖然這些頁中的數據都是隨機產生的垃圾數據,不過,雖然概率很低,但是也有可能會包含某些敏感信息。所以,更謹慎些,我們可以將獲得的頁都填充為0。這會用到get_zeroed_page函數。而這個函數又用到了__get_free_pages函數。
所以這三個函數最終都是使用了alloc_pages函數。
釋放頁
當我們不再需要某些頁時可以使用下面的函數釋放它們:
__free_pages(struct page *page, unsigned int order)
free_pages(unsigned long addr, unsigned int order)
free_page(unsigned long addr)
以上這些接口都是以頁為單位進行內存分配與釋放的。
kmalloc與vmalloc
在實際中內核需要的內存不一定是整個頁,可能只是以字節為單位的一片區域。這兩個函數就是實現這樣的目的。
不同之處在於,kmalloc分配的是虛擬地址連續,物理地址也連續的一片區域,vmalloc分配的是虛擬地址連續,物理地址不一定連續的一片區域。
對應的釋放內存的函數是kfree與vfree。
4、slab層
以頁為最小單位分配內存對於內核管理系統中的物理內存來說的確比較方便,但內核自身最常使用的內存卻往往是很小的內存塊——比如存放文件描述符、進程描述符、虛擬內存區域描述符等行為所需的內存都遠不及一頁,一個整頁中可以聚集多個這些小塊內存。
為了滿足內核對這種小內存塊的需要,Linux系統采用了一種被稱為slab分配器(也稱作slab層)的技術。slab分配器的實現相當復雜,但原理不難,其核心思想就是“存儲池”的運用。內存片段(小塊內存)被看作對象,當被使用完后,並不直接釋放而是被緩存到“存儲池”里,留做下次使用,這無疑避免了頻繁創建與銷毀對象所帶來的額外負載。
slab分配器扮演了通用數據結構緩存層的角色。
slab層把不同的對象划分為所謂高速緩存組,其中每個高速緩存組都存放不同類型的對象,每種對象對應一個高速緩存。
常見的高速緩存組有:進程描述符(task_struct結構體),索引節點對象(struct inode),目錄項對象(struct dentry),通用頁對象等等。
這些高速緩存又被划分為slab。slab由一個或多個物理連續的頁組成,一般僅僅由一頁組成。每個高速緩存可以由多個slab(頁)組成。
每個高速緩存都使用struct kmem_cache結構表示,這個結構包含三個鏈表:slabs_full、slabs_partial和slabs_empty,均放在kmem_list3結構體內。這些鏈表的每個元素為slab描述符即struct slab結構體。
每個高速緩存需要創建新的slab即新的頁,還是通過上面提到的__get_free_page()來實現的。通過最終調用free_pages()釋放內存頁。
一個高速緩存的創建和銷毀使用kmem_cache_create與kmem_cache_destroy。
高速緩存中的對象的分配和釋放使用kmem_cache_alloc與kmem_cache_free。
從上看出,slab層仍然是建立在頁的基礎之上,可以總結為slab層將 空閑頁 分解成 眾多相同長度的小塊內存 以供 同類型的數據結構 使用。
5、進程地址空間
以上我們講述了內核如何管理內存,內核內存分配機制包括了頁分配器和slab分配器。內核除了管理本身的內存外,也必須管理用戶空間中進程的內存。
我們稱這個內存為進程地址空間,也就是系統中每個用戶空間進程所看到的內存。Linux系統采用虛擬內存技術,所有進程以虛擬方式共享內存。Linux中主要采用分頁機制而不是分段機制。
5.1 地址空間布局
進程內存區域可以包含各種內存對象,從下往上依次為:
(1)可執行文件代碼的內存映射,稱為代碼段。只讀可執行。
(2)可執行文件的已初始化全局變量的內存映射,稱為數據段。后續都是可讀寫。
(3)包含未初始化的全局變量,就是bass段的零頁的內存映射。
(4)堆區,動態內存分配區域;包括任何匿名的內存映射,比如malloc分配的內存。
(5)棧區,用於進程用戶空間棧的零頁內存映射,這里不要和進程內核棧混淆,進程的內核棧獨立存在並由內核維護,因為內核管理着所有進程。所以內核管理着內核棧,內核棧管理着進程。
(6)其他可能存在的:內存映射文件;共享內存段;C庫或者動態鏈接庫等共享庫的代碼段、數據段和bss也會被載入進程的地址空間。
5.2 內存描述符
內核使用內存描述符mm_struct結構體表示進程的地址空間,該結構體包含了和進程地址空間有關的全部信息。
1 struct mm_struct { 2 struct vm_area_struct *mmap; /* list of memory areas */ 3 struct rb_root mm_rb; /* red-black tree of VMAs */ 4 struct vm_area_struct *mmap_cache; /* last used memory area */ 5 unsigned long free_area_cache; /* 1st address space hole */ 6 pgd_t *pgd; /* page global directory */ 7 atomic_t mm_users; /* address space users */ 8 atomic_t mm_count; /* primary usage counter */ 9 int map_count; /* number of memory areas */ 10 struct rw_semaphore mmap_sem; /* memory area semaphore */ 11 spinlock_t page_table_lock; /* page table lock */ 12 struct list_head mmlist; /* list of all mm_structs */ 13 unsigned long start_code; /* start address of code */ 14 unsigned long end_code; /* final address of code */ 15 unsigned long start_data; /* start address of data */ 16 unsigned long end_data; /* final address of data */ 17 unsigned long start_brk; /* start address of heap */ 18 unsigned long brk; /* final address of heap */ 19 unsigned long start_stack; /* start address of stack */ 20 unsigned long arg_start; /* start of arguments */ 21 unsigned long arg_end; /* end of arguments */ 22 unsigned long env_start; /* start of environment */ 23 unsigned long env_end; /* end of environment */ 24 unsigned long rss; /* pages allocated */ 25 unsigned long total_vm; /* total number of pages */ 26 unsigned long locked_vm; /* number of locked pages */ 27 unsigned long def_flags; /* default access flags */ 28 unsigned long cpu_vm_mask; /* lazy TLB switch mask */ 29 unsigned long swap_address; /* last scanned address */ 30 unsigned dumpable:1; /* can this mm core dump? */ 31 int used_hugetlb; /* used hugetlb pages? */ 32 mm_context_t context; /* arch-specific data */ 33 int core_waiters; /* thread core dump waiters */ 34 struct completion *core_startup_done; /* core start completion */ 35 struct completion core_done; /* core end completion */ 36 rwlock_t ioctx_list_lock; /* AIO I/O list lock */ 37 struct kioctx *ioctx_list; /* AIO I/O list */ 38 struct kioctx default_kioctx; /* AIO default I/O context */ 39 };
mmap和mm_rb描述的對象是一樣的:該地址空間中全部內存區域(all memory areas)。
mmap是以鏈表的形式存放,而mm_rb是以紅黑樹存放,前者有利於遍歷所有數據,而后者有利於快速搜索定位到某個地址。所有的mm_struct結構體都通過自身的mmlist域連接在一個雙向鏈表中,該鏈表的首元素是init_mm內存描述符,它代表init進程的地址空間。
再往下看,可以看到地址空間幾個區(堆棧)對應的變量的定義。
我們再回顧下在內核進程管理中,進程描述符task_struct是在內核空間中緩存,也就是我們上面描述的slab層。
而task_struct中有個mm域指向的就是該進程使用的內存描述符,再通過current->mm便可以指向當前進程的內存描述符。fork函數利用copy_mm()函數就實現了復制父進程的內存描述符,而子進程中的mm_struct結構體實際是通過文件kernel/fork.c中的allocate_mm()宏從mm_cachep slab緩存中分配得到的。通常,每個進程都有唯一的mm_struct結構體。
因為進程描述符和進程的內存描述符都是處於slab層,所以它們元素的分配和釋放都由slab分配器來管理。
5.3 虛擬內存區域
內存區域由vm_area_struct結構體描述,見上面的mmap域,內存區域在內核中也經常被稱作虛擬內存區域(Virtual Memory Area,VMA)。
它描述了指定地址空間內連續區間上的一個獨立內存范圍。
內核將每個內存區域作為一個單獨的內存對象管理,每個內存區域都擁有一致的屬性。結構體如下:
1 struct vm_area_struct { 2 struct mm_struct *vm_mm; /* associated mm_struct */ 3 unsigned long vm_start; /* VMA start, inclusive */ 4 unsigned long vm_end; /* VMA end , exclusive */ 5 struct vm_area_struct *vm_next; /* list of VMA's */ 6 pgprot_t vm_page_prot; /* access permissions */ 7 unsigned long vm_flags; /* flags */ 8 struct rb_node vm_rb; /* VMA's node in the tree */ 9 union { /* links to address_space->i_mmap or i_mmap_nonlinear */ 10 struct { 11 struct list_head list; 12 void *parent; 13 struct vm_area_struct *head; 14 } vm_set; 15 struct prio_tree_node prio_tree_node; 16 } shared; 17 struct list_head anon_vma_node; /* anon_vma entry */ 18 struct anon_vma *anon_vma; /* anonymous VMA object */ 19 struct vm_operations_struct *vm_ops; /* associated ops */ 20 unsigned long vm_pgoff; /* offset within file */ 21 struct file *vm_file; /* mapped file, if any */ 22 void *vm_private_data; /* private data */ 23 };
每個內存描述符都對應於地址進程空間中的唯一區間。vm_mm域指向和VMA相關的mm_struct結構體。
一個內存區域的地址范圍是[vm_start, vm_end),vm_next指向該進程的下一個內存區域。
兩個獨立的進程將同一個文件映射到各自的地址空間,它們分別都會有一個vm_area_struct結構體來標志自己的內存區域;但是如果兩個線程共享一個地址空間,那么它們也同時共享其中的所有vm_area_struct結構體。
在上面的vm_flags域中存放的是VMA標志,標志了內存區域所包含的頁面的行為和信息。和物理頁訪問權限不同,VMA標志反映了內核處理頁面所需要遵循的行為准則,而不是硬件要求。而且vm_flags同時包含了內存區域中每個頁面的消息或者內存區域的整體信息,而不是具體的獨立頁面。如下表所述:
開頭三個標志表示代碼在該內存區域的可讀、可寫和可執行權限。
第四個標志VM_SHARD說明了該區域包含的映射是否可以在多進程間共享,如果被設置了,表示共享映射;否則未被設置,表示私有映射。
其中很多狀態在實際使用中都非常有用。
5.4 mmap()和do_mmap():創建地址空間
內核使用do_mmap()函數創建一個新的線性地址空間。但如果創建的地址區間和一個已經存在的地址區間相鄰,並且它們具有相同的訪問權限的話,那么兩個區間將合並為一個。如果不能合並,那么就確實需要創建一個新的vma了,但無論哪種情況,do_mmap()函數都會將一個地址區間加入到進程的地址空間中。這個函數定義在linux/mm.h中,如下
unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot,unsigned long flag, unsigned long offset)
這個函數中由file指定文件,具體映射的是文件中從偏移offset處開始,長度為len字節的范圍內的數據,如果file參數是NULL並且offset參數也是0,那么就代表這次映射沒有和文件相關,該情況被稱作匿名映射(file-backed mapping)。如果指定了文件和偏移量,那么該映射被稱為文件映射(file-backed mapping)。
其中參數prot指定內存區域中頁面的訪問權限:可讀、可寫、可執行。
flag參數指定了VMA標志,這些標志指定類型並改變映射的行為,請見上一小節。
如果系統調用do_mmap的參數中有無效參數,那么它返回一個負值;否則,它會在虛擬內存中分配一個合適的新內存區域,如果有可能的話,將新區域和臨近區域進行合並,否則內核從vm_area_cachep長字節(slab)緩存中分配一個vm_area_struct結構體,並且使用vma_link()函數將新分配的內存區域添加到地址空間的內存區域鏈表和紅黑樹中,隨后還要更新內存描述符中的total_vm域,然后才返回新分配的地址區間的初始地址。
在用戶空間,我們可以通過mmap()系統調用獲取內核函數do_mmap()的功能。
5.5 munmap()和do_munmap():刪除地址空間
do_mummp()函數從特定的進程地址空間中刪除指定地址空間,該函數定義在文件linux/mm.h中,如下:
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
第一個參數指定要刪除區域所在的地址空間,刪除從地址start開始,長度為len字節的地址空間,如果成功,返回0,否則返回負的錯誤碼。
與之相對應的用戶空間系統調用是munmap,它是對do_mummp()函數的一個簡單封裝。
5.6 malloc()的實現
我們知道malloc()是C庫中實現的。C庫對內存分配的管理還有calloc()、realloc()、free()等函數。
事實上,malloc函數是以brk()或者mmap()系統調用實現的。
brk和sbrk主要的工作是實現虛擬內存到內存的映射。在Linux系統上,程序被載入內存時,內核為用戶進程地址空間建立了代碼段、數據段和堆棧段,在數據段與堆棧段之間的空閑區域用於動態內存分配。我們回到內存結構mm_struct中的成員變量start_code和end_code是進程代碼段的起始和終止地址,start_data和 end_data是進程數據段的起始和終止地址,start_stack是進程堆棧段起始地址,start_brk是進程動態內存分配起始地址(堆的起始地址),還有一個 brk(堆的當前最后地址),就是動態內存分配當前的終止地址。所以C庫的malloc()在Linux上的基本實現是通過內核的brk系統調用。brk()是一個非常簡單的系統調用,內核再執行sys_brk()函數進行內存分配,只是簡單地改變mm_struct結構的成員變量brk的值。而sbrk不是系統調用,是C庫函數。系統調用通常提供一種最小功能,而庫函數通常提供比較復雜的功能。
下面我們整理一下在進程空間堆中用brk()方式進行動態內存分配的流程:
C庫函數malloc()調用Linux系統調用函數brk(),brk()執行系統調用陷入到內核,內核執行sys_brk()函數,sys_brk()函數調用do_brk()進行內存分配
malloc()---------->brk()-----|----->sys_brk()----------->do_brk()------------>vma_merge()/kmem_cache_zalloc()
用戶空間------> | 內核空間
系統調用---------->
mmap()系統調用也可以實現動態內存分配功能,即5.4節我們提到的匿名映射。
那什么時候調用brk(),什么時候調用mmap()呢?通過閾值M_MMAP_THRESHOLD
來決定。該值默認128KB。可以通過mallopt()來進行修改設置。
所以當需要分配的內存大於該閾值,選擇mmap();否則小於等於該閾值,選擇brk()分配。
最后,mmap分配的內存在調用munmap后會立即返回給系統,而brk/sbrk而受M_TRIM_THRESHOLD的影響。該環境變量同樣通過mallopt()來設置,該值代表的意義是釋放內存的最少字節數。
此時,str1和str2的內存只是簡單的標記為“未使用”,如果這兩處內存是相鄰的則會進行合並,這種算法也稱為“伙伴內存算法(buddy memory allocation scheme)”。這種算法高速簡單,但同時也會生成碎片。包括內碎片(一次分配內存不夠整頁,最后一頁剩下的空間)和外碎片(多次或者反復分配造成中間的空閑頁太小不夠后續的一次分配)。
從上可以看出,在一定條件下,假如釋放了str3的內存,堆的大小是可以緊縮的。
最后我們以一張圖結束今天的主題,內存分配流程圖:
推薦閱讀:存儲系列之 虛擬內存:分頁技術
參考資料:
《Linux內核設計與實現》原書第三版
https://www.cnblogs.com/bizhu/archive/2012/10/09/2717303.html