1.內存管理概述linux
虛擬地址又叫線性地址。linux沒有采用分段機制,因此邏輯地址和虛擬地址(線性地址)是一個概念。內核的虛擬地址和物理地址,大部分只差一個線性偏移量。用戶空間的虛擬地址和物理地址則採用了多級頁表進行映射,但仍稱之爲線性地址。算法
2.進程內存空間緩存
毫無疑問,全部進程都必須佔用必定數量的內存,它或是用來存放從磁盤載入的程序代碼,或是存放取自用戶輸入的數據等等。不過進程對這些內存的管理方式因內存用途不一而不盡相同,有些內存是事先靜態分配和統一回收的,而有些倒是按須要動態分配和回收的。數據結構
Linux操做系統採用虛擬內存管理技術,使得每一個進程都有各自互不干涉的進程地址空間,該空間是塊大小爲4G的線性虛擬空間,用戶所看到和接觸到的都是該虛擬地址,沒法看到實際的物理內存地址。利用這種虛擬地址不但能起到保護操做系統的效果(用戶不能直接訪問物理內存),並且更重要的是,用戶程序可以使用比實際物理內存更大的地址空間。函數
4G的進程地址空間被人爲的分爲兩個部分——用戶空間與內核空間。用戶空間從0到3G(0xC0000000),內核空間佔據3G到4G。用戶進程一般狀況下只能訪問用戶空間的虛擬地址,不能訪問內核空間虛擬地址。只有用戶進程進行系統調用(表明用戶進程在內核態執行)等時刻能夠訪問到內核空間。性能
每當進程切換,用戶空間就會跟着變化;而內核空間是由內核負責映射,它並不會跟着進程改變,是固定的。內核空間地址有本身對應的頁表(init_mm.pgd),用戶進程各自有不一樣的頁表。spa
對任何一個普通進程來說,它都會涉及到5種不一樣的數據段:操作系統
[1]代碼段:代碼段是用來存放可執行文件的操做指令,也就是說是它是可執行程序在內存中的鏡像。代碼段須要防止在運行時被非法修改,因此只准許讀取操做,而不容許寫入(修改)操做——它是不可寫的。翻譯
[2]已初始化數據段(.data):數據段用來存放可執行文件中已初始化全局變量,換句話說就是存放程序靜態分配的變量和全局變量。對象
[3]未初始化數據段(.bss):BSS段包含了程序中未初始化的全局變量,在內存中bss段所有置零。
[4]堆(heap):堆是用於存放進程運行中動態分配的內存段。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)。
[5]棧:棧是用戶存放程序臨時建立的局部變量,也就是說咱們函數括弧「{}」中定義的變量(但不包括static聲明的變量,static意味着在數據段中存放變量)。除此之外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,而且待到調用結束後,函數的返回值也會被存放回棧中。因爲棧的先進先出特色,因此棧特別方便用來保存/恢復調用現場。從這個意義上講,咱們能夠把堆棧當作一個寄存、交換臨時數據的內存區。
3.進程內存管理
很明顯,進程內存管理的對象是進程使用的虛擬內存區域,爲了方便管理,虛擬空間被劃分爲許多大小可變的(但必須是4096的倍數)內存區域,這些區域的劃分原則是「將訪問屬性一致的地址空間存放在一塊兒」,所謂訪問屬性在這裏無非指的是「可讀、可寫、可執行等」。
要查看某個進程佔用的內存區域,可使用命令cat /proc/<pid>/maps得到。
vm_area_struct是描述進程地址空間的基本管理單元,對於一個進程來講每每須要多個內存區域來描述它的虛擬空間,vm_area_struct結構是以鏈表形式連接,不過爲了方便查找,內核又以紅黑樹(之前的內核使用平衡樹)的形式組織內存區域,以便下降搜索耗時。並存的兩種組織形式,並不是冗餘:鏈表用於須要遍歷所有節點的時候用,而紅黑樹適用於在地址空間中定位特定內存區域的時候。內核爲了內存區域上的各類不一樣操做都能得到高性能,因此同時使用了這兩種數據結構。
下圖反映了進程地址空間的管理模型:
建立進程fork()、程序載入execve()、映射文件mmap()、動態內存分配malloc()/brk()等進程相關操做都須要分配內存給進程。不過這時進程申請和得到的還不是物理內存,而是虛擬內存。進程對虛擬內存的分配最終都會歸結到do_mmap()函數上來(brk調用被單獨以系統調用實現,不用do_mmap()),內核使用do_mmap()函數建立一個新的線性地址區間,可是說該函數建立了一個新vm_area_struct並不很是準確,由於若是建立的地址區間和一個已經存在的地址區間相鄰,而且它們具備相同的訪問權限的話,那麼兩個區間將合併爲一個。若是不能合併,那麼就確實須要建立一個新的vm_area_struct了,但不管哪一種狀況,do_mmap()函數都會將一個地址區間加入到進程的地址空間中,不管是擴展已存在的內存區域仍是建立一個新的區域。
一樣,釋放一個內存區域應使用函數do_ummap(),它會銷燬對應的內存區域。
進程所能直接操做的地址都爲虛擬地址。當進程須要內存時,從內核得到的僅僅是虛擬的內存區域,而不是實際的物理地址,得到的僅僅是對一個新的線性地址區間的使用權。實際的物理內存只有當進程真的去訪問新獲取的虛擬地址時,纔會由「請求頁機制」產生「缺頁」異常,從而進入分配實際頁面的例程。該異常是虛擬內存機制賴以存在的基本保證——它會告訴內核去真正爲進程分配物理頁,並創建對應的頁表,這以後虛擬地址才實實在在地映射到了系統的物理內存上。(固然,若是頁被換出到磁盤,也會產生缺頁異常,不過這時不用再創建頁表了)。
總之,當訪問的進程虛擬內存並未真正分配頁面時,該操做便被調用來分配實際的物理頁,併爲該頁創建頁表項。
雖然應用程序操做的對象是映射到物理內存之上的虛擬內存,可是處理器直接操做的倒是物理內存。因此當應用程序訪問一個虛擬地址時,首先必須將虛擬地址轉化成物理地址,而後處理器才能解析地址訪問請求。地址的轉換工做須要經過查詢頁表才能完成,歸納地講,地址轉換須要將虛擬地址分段,使每段虛地址都做爲一個索引指向頁表,而頁表項則指向下一級別的頁表或者指向最終的物理頁面。
每一個進程都有本身的頁表。進程描述符的pgd域指向的就是進程的頁全局目錄。
4.物理內存管理
Linux內核管理物理內存是經過分頁機制實現的,它將整個內存劃分紅無數個4k(在i386體系結構中)大小的頁,從而分配和回收內存的基本單位即是內存頁了。利用分頁管理有助於靈活分配內存地址,由於分配時沒必要要求必須有大塊的連續內存。雖然如此,可是實際上系統使用內存時仍是傾向於分配連續的內存塊,由於分配連續內存時,頁表不須要更改,所以能下降TLB的刷新率(頻繁刷新會在很大程度上下降訪問速度)。
內核分配物理頁面時爲了儘可能減小不連續狀況,採用了buddy算法來管理空閒頁面。內核中分配空閒頁面的基本函數是基於buddy的get_free_page/get_free_pages,它們或是分配1頁或是分配指定的頁面(二、四、8…512頁)。
注意:get_free_page是在內核中分配內存,不一樣於malloc在用戶空間中分配,malloc利用堆動態分配,其實是調用brk()系統調用,該調用的做用是擴大或縮小進程堆空間(它會修改進程的brk域)。若是現有的內存區域不夠容納堆空間,則會以頁面大小的倍數爲單位,擴張或收縮對應的內存區域,但brk值並不是以頁面大小爲倍數修改,而是按實際請求修改。所以Malloc在用戶空間分配內存能夠以字節爲單位分配,但內核在內部仍然會是以頁爲單位分配的。
由get_free_page函數所分配的連續內存都陷於物理映射區域,因此它們返回的內核虛擬地址和實際物理地址僅僅是相差一個偏移量(PAGE_OFFSET),你能夠很方便的將其轉化爲物理內存地址,同時內核也提供了virt_to_phys()函數將內核虛擬空間中的物理映射區地址轉化爲物理地址。
夥伴關係也好、slab技術也好,從內存管理理論角度而言目的基本是一致的,它們都是爲了防止「分片」。slab分配器使得一個頁面內包含的衆多小塊內存可獨立被分配使用,避免了內部分片,節約了空閒內存。夥伴關係把內存塊按大小分組管理,必定程度上減輕了外部分片的危害,由於頁框分配不在盲目,而是按照大小依次有序進行,不過夥伴關係只是減輕了外部分片,但並未完全消除。
【1】內部碎片和外部碎片
內部分片是說系統爲了知足一小段內存區(連續)的須要,不得不分配了一大區域連續內存給它,從而形成了空間浪費;
外部分片是指系統雖有足夠的內存,但倒是分散的碎片,沒法知足對大塊「連續內存」的需求。
【2】buddy算法
夥伴算法(Buddy system)把全部的空閒頁框分爲11個塊鏈表,每塊鏈表中分佈包含特定的連續頁框地址空間,好比第0個塊鏈表包含大小爲2^0個連續的頁框,第1個塊鏈表中,每一個鏈表元素包含2個頁框大小的連續地址空間,….,第10個塊鏈表中,每一個鏈表元素表明4M的連續地址空間。每一個鏈表中元素的個數在系統初始化時決定,在執行過程當中,動態變化。
夥伴算法每次只能分配2的冪次個頁框的空間,好比一次分配1頁,2頁,4頁,8頁,…,1024頁(2^10)等等,每頁大小通常爲4K,所以,夥伴算法最多一次可以分配4M(1024*4K)的內存空間。
一般X86系統上MAX_ORDER是11,限制了最大分配的連續物理內存頁爲1024頁,即4M。
初始階段,每一個鏈表中元素的個數是固定的,假如MAX_ORDER是11,則此時可能第10個塊裏面都是4M的塊鏈表,其餘塊的鏈表爲空,當分配內存的時候就向下拆分,當釋放的時候就向上合併,所以這種方式能夠有效的保持必定量的連續物理內存塊,方便某些狀況申請到大塊的物理內存。
所以,夥伴算法的優勢就是,較好的解決了外部碎片問題。不過夥伴關係只是減輕了外部分片,但並未完全消除。
同時致使的問題:一個連續的內存中僅僅一個頁面被佔用,致使整塊內存區都不能合併;會致使內部碎片,例如須要9K大小時,必須分配16K的內存空間,但實際只用到9K。
【3】slab
以頁爲最小單位分配內存對於內核管理系統中的物理內存來講的確比較方便,但內核自身最常使用的內存卻每每是很小(遠遠小於一頁)的內存塊——好比存放文件描述符、進程描述符、虛擬內存區域描述符等行爲所需的內存都不足一頁。 爲了知足內核對這種小內存塊的須要,Linux系統採用了一種被稱爲slab分配器的技術。Slab分配器的核心思想就是「存儲池」的運用。內存片斷(小塊內存)被看做對象,當被使用完後,並不直接釋放而是被緩存到「存儲池」裏,留作下次使用,這無疑避免了頻繁建立與銷燬對象所帶來的額外負載。
slab創建在頁面和buddy算法基礎之上,每一個Slab都是從buddy分配的連續的物理頁面。slab中的對象分配和銷燬使用kmem_cache_alloc與kmem_cache_free。
linux在slab基礎上構造了一個通用的內存分配器,即kmalloc函數,系統在內存初始化的時候就預先創建一組預約義大小的kmen_cache,從32字節開始,按2的冪逐步遞增,用戶調用kmalloc進行內存分配時,就是從kmem_cache中找到第一個比size大的kmem_cache。所以用kmalloc申請50個字節時,實際上是從size-64的slab中進行分配的。
【4】vmalloc
kmalloc函數基於slab分配的內存對象和get_free_page/get_free_pages申請的內存,最後都是基於buddy獲得的連續的物理內存,經過buddy分配的內存由MAX_ORDER限制,所以不能分配超大內存。所以內核提供了vmalloc函數用來分配超大內存。
函數vmalloc返回一個連續的虛擬地址的內存塊,可是物理塊多是不連續的。
相比kmalloc來講,vmalloc須要創建專門的頁表項,對不連續的物理內存進行映射,所以分配效率要低。因此通常用kmalloc進行內存分配,只有分配超大內存時才使用vmalloc。
總結:最底層的物理內存分配是基於buddy算法的get_free_page/get_free_pages,以後是基於buddy算法的slab分配內存kmem_cache_alloc與kmem_cache_free,在上面就是基於slab的kmalloc函數。
以後是基於buddy算法的vmalloc。
【5】多級頁表結構
進程訪問的對象都是虛擬地址,CPU經過MMU把虛擬地址轉爲物理地址訪問真正的物理內存。虛擬內存和物理內存的映射關係保存在頁表中。使用多級頁表能夠節約地址轉換須要的空間。
爲何不分級的頁表就作不到節約內存呢?這是保存在主存中的頁表承擔的職責是將虛擬地址翻譯成物理地址,因此頁表必定要覆蓋所有虛擬地址空間,不分級的頁表就須要有1M個頁表項來映射,而二級頁表則最少只須要1K個頁表項。
目前是四層頁表結構:頁全局目錄、頁上級目錄、頁中間目錄、頁表項。進程地址描述符中記錄的mm->pgd就是當前進程的全局頁目錄的起始地址。
問題:虛擬空間上,每一個頁面爲4K,所以4G的內存須要有1M個頁表項(4GB / 4KB = 1M)。若是使用二級頁表,一級頁表映射4MB,二級頁表映射4Kb,則須要1K + 1K * 1K = 1.1M,佔用空間反而大了,這什麼緣由?
這是由於一級頁表確定存在,可是二級頁表能夠不存在,由於若是一級的頁表沒有用到,就不用建立其對應的二級頁表了,能夠在須要的時候建立,即一級頁表覆蓋到了所有虛擬地址空間,二級頁表在須要時建立。
同理,多級頁表能夠更加節約空間,同時多級頁表也是典型的時間換空間的例子,動態建立二級頁表、調入和調出二級頁表都是須要花費額外時間的,遠沒有不分級的頁表來的直接;