前面幾篇介紹了進程的一些知識,從這篇開始介紹內存、文件、IO等知識,發現更很差寫哈哈。但仍是有必要記錄下本身的所學所思。供後續翻閱,同時寫做也是一個鞏固的過程。html
這些知識之前有文檔涉及過,可是角度不一樣,這個系列站的角度更底層,基本都是從Linux內核出發,會更深刻。因此當你都讀完,而後再次審視這些功能的實現和設計時,我相信你會有種豁然開朗的感受。node
內核把物理頁做爲內存管理的基本單元。linux
儘管處理器的最小處理單位是字(或者字節),可是MMU(內存管理單元,管理內存並把虛擬地址轉換爲物理地址的硬件)一般以頁爲單位進行處理。因此從虛擬內存看,頁也是最小單元。算法
體系不一樣,支持的頁大小不一樣。大多數32位體系結構支持4KB的頁,而64位體系結構通常會支持8KB的頁。緩存
內核用struct page結構體表示系統中的每一個頁,包含不少項好比頁的狀態(有沒有髒,有沒有被鎖定)、引用計數(-1表示沒有使用)等等。數據結構
page結構和物理頁相關,和虛擬內存無關。因此它的描述是短暫的,僅僅記錄當前的使用情況,固然也不會描述其中的數據。app
內核用這個結構來管理系統中全部的頁,因此內核知道哪些頁是空閒的,若是在使用中擁有者又是誰。函數
這個擁有者有四種:用戶空間進程、動態分配內存的內核數據、靜態內核代碼以及頁高速緩存。佈局
有些頁是有特定用途的。好比內存中有些頁是專門用於DMA的。this
內核使用區的概念將具備類似特性的頁進行分組。區是一種邏輯上的分組的概念,而沒有物理上的意義。
區的實際使用和分佈是與體系結構相關的。在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表示。
得到頁使用的接口是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分配的是虛擬地址連續,物理地址不必定連續的一片區域。
對應的釋放內存的函數是kfree與vfree。
以頁爲最小單位分配內存對於內核管理系統中的物理內存來講的確比較方便,但內核自身最常使用的內存卻每每是很小的內存塊——好比存放文件描述符、進程描述符、虛擬內存區域描述符等行爲所需的內存都遠不及一頁,一個整頁中能夠彙集多個這些小塊內存。
爲了知足內核對這種小內存塊的須要,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層將 空閒頁 分解成 衆多相同長度的小塊內存 以供 同類型的數據結構 使用。
以上咱們講述了內核如何管理內存,內核內存分配機制包括了頁分配器和slab分配器。內核除了管理自己的內存外,也必須管理用戶空間中進程的內存。
咱們稱這個內存爲進程地址空間,也就是系統中每一個用戶空間進程所看到的內存。Linux系統採用虛擬內存技術,全部進程以虛擬方式共享內存。Linux中主要採用分頁機制而不是分段機制。
進程內存區域能夠包含各類內存對象,從下往上依次爲:
(1)可執行文件代碼的內存映射,稱爲代碼段。只讀可執行。
(2)可執行文件的已初始化全局變量的內存映射,稱爲數據段。後續都是可讀寫。
(3)包含未初始化的全局變量,就是bass段的零頁的內存映射。
(4)堆區,動態內存分配區域;包括任何匿名的內存映射,好比malloc分配的內存。
(5)棧區,用於進程用戶空間棧的零頁內存映射,這裏不要和進程內核棧混淆,進程的內核棧獨立存在並由內核維護,由於內核管理着全部進程。因此內核管理着內核棧,內核棧管理着進程。
(6)其餘可能存在的:內存映射文件;共享內存段;C庫或者動態連接庫等共享庫的代碼段、數據段和bss也會被載入進程的地址空間。
內核使用內存描述符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分配器來管理。
內存區域由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說明了該區域包含的映射是否能夠在多進程間共享,若是被設置了,表示共享映射;不然未被設置,表示私有映射。
其中不少狀態在實際使用中都很是有用。
內核使用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()的功能。
do_mummp()函數從特定的進程地址空間中刪除指定地址空間,該函數定義在文件linux/mm.h中,以下:
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
第一個參數指定要刪除區域所在的地址空間,刪除從地址start開始,長度爲len字節的地址空間,若是成功,返回0,不然返回負的錯誤碼。
與之相對應的用戶空間系統調用是munmap,它是對do_mummp()函數的一個簡單封裝。
咱們知道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