1. 程式人生 > >Linux 內存管理

Linux 內存管理

point 兩種 tac reg core 種類型 brk() term 閾值

Linux將它的物理內存虛擬化。進程並不能直接在物理內存上尋址,而是由Linux內核為每個進程維護一個特殊的虛擬地址空間(virtual address space)。這個地址空間是線性的,從0開始,到某個最大值。虛擬空間由許多頁組成。系統的體系結構以及機型決定了頁的大小(頁的大小是固定的),典型的頁的大小包括4K(32位系統)和8K(64位系統)。每個頁面都只有無效(invalid)和有效(valid)這兩種狀態,
有效頁面(valid page)和一個物理頁或者一些二級存儲介質相關聯,例如一個交換分區或者一個在硬盤上的文件。
無效頁面(invalid page)沒有關聯,代表它沒有被分配或使用。對無效頁面的訪問會引發一個段錯誤。
地址空間不需要是連續的。雖然是線性編址,但實際上中間有很多未編址的小區域。一個進程不能訪問一個處在二級存儲中的頁,除非這個頁和物理內存中的頁相關聯。如果一個進程嘗試訪問這樣的頁面,那麽存儲器管理單元(MMU)會產生一個頁錯誤(page fault)。

虛存中的多個頁面,甚至是屬於不同進程的虛擬地址空間,也有可能被映射到同一個物理頁面。這樣允許不同的虛擬地址空間共享(share)物理內存上的數據。共享的數據可能是只讀的,或者是可讀可寫的。當一個進程試圖寫某個共享的可寫頁時,另一種情況是MMU會截取這次寫操作並產生一個異常;作為回應,內核就會透明的創造一份這個頁的拷貝以供該進程進行寫操作。我們將這種方法稱為寫時拷貝(copy-on-write)(COW)。

內核將具有某些相同特征的頁組織成塊(blocks),例如讀寫權限。這些塊叫做存儲器區域(memory regions),段(segments),或者映射(mappings)。典型的段包括:
1,文本段(text segment)包含著一個進程的代碼,字符串,常量和一些只讀的數據。在Linux中,文本段被標記為只讀,並且直接從目標文件(可執行程序或是庫文件)映射到內存中。
2,堆棧段(stack)包括一個進程的執行棧,隨著棧的深度動態的伸長或收縮。執行棧中包括了程序的局部變量(local variables)和函數的返回值。
3,數據段(data segment),又叫堆(heap),包含著一個進程的動態存儲空間。這個段是可寫的,而且它的大小是可以變化的。這部分空間往往是由malloc分配的。

4,BSS段(bss segment)包含了沒有被初始化的全局變量。這些變量根據不同的C標準都有特殊的值,通常來說,都是0。

動態內存分配
void malloc (size_t size);
void
calloc (size_t nr, size_t size);
void realloc (void ptr, size_t size);
void free (void *ptr);
malloc()時會得到一個size大小的內存區域,並返回一個指向這部分內存首地址的指針。這塊內存區域的內容是未定義的,不要自認為全是0。失敗時,malloc()返回NULL,並設置errno錯誤值為ENOMEM。
數組分配calloc與malloc不同的是,calloc將分配的區域全部用0進行初始化。要註意的是二進制0和和浮點0是不一樣的。
調整已分配內存大小realloc成功調用realloc()將ptr指向的內存區域的大小變為size字節。它返回一個指向新空間的指針,當試圖擴大內存塊的時候返回的指針可能不再是ptr。因為有潛在的拷貝操作,如果size是0,效果就會跟在ptr上調用free()相同。

調用free()會釋放ptr指向的內存。但ptr必須是之前調用malloc(),calloc(),或者realloc()的返回值。也就是說,你不能用free()來釋放申請到的部分內存,比如說用一個指針指向一塊空間中間的位置。ptr可能是NULL這個時候free()什麽都不做就返回了,因此調用free()時並不需要檢查ptr是否為NULL。內存泄漏和懸垂指針有兩個常用的工具可以幫助你解決這些問題:Electric Fence和valgrind 。

數據的對齊(alignment)是指數據地址和由硬件確定的內存塊之間的關系。一個變量的地址是它大小的倍數時,就叫做自然對齊(naturally aligned)。POSIX1003.1d提供一個叫做posix_memalign()的函數。BSD和SunOS分別提供了如下接口:valloc除了標準類型的對齊和內存分配,對齊問題還可以進行擴展。比如說,復雜的數據類型的對齊問題將會比標準類型的更復雜。

數據段的管理

堆的起始地址空間在由操作系統和執行文件大小決定的。
int brk(void addr);
void
sbrk (intptr_t increment);
因為malloc()和其它的方法更強大也易於使用,大多數程序都不會直接地使用這些接口。sbrk老版本Unix系統中函數的名字,那時堆和棧還在同一個段中。堆中動態存儲器的分配由數據段的底部向上生長;棧從數據段的頂部向著堆往下生長。堆和棧的分界線叫做中斷(break)或中斷點(break point)。在現代系統中,數據段存在於它自己的內存映射中,我們仍用中斷點來標記映射的結束地址。調用brk()會設置中斷點(數據段的末端)的地址為end。在成功的時候,返回0。失敗的時候,返回-1,並設置errno為ENOMEM。調用sbrk()將數據段末端增加increment字節,increment可正可負。sbrk()返回修改後的斷點。所以,increment為0時得到的是現在斷點的地址。

匿名存儲器映射
Glibc的內存分配使用了數據段和內存映射。實現malloc()最經典方法就是將數據段分為一系列的大小為2的冪的塊,返回最小的符合要求的那個塊來滿足請求。釋放則只是簡單的將這塊區域標記為未使用。如果相鄰的分區都是空閑的,他們會被合成一個更大的分區。如果堆的最頂端是空的,系統可以用brk()來降低斷點,使堆收縮,將內存返回給系統。這個算法叫做夥伴內存分配算法(buddymemoryallocationscheme)。它的優點是高速和簡單,缺點則是會產生兩種類型的碎片。
1,當使用的內存塊大於請求的大小時則產生內部碎片(Internal fragmentation)。
2,外部碎片是在空閑存儲器合計起來夠滿足一個請求,但是沒有一個單獨的空間塊可以來處理這個請求時發生的。這同樣會導致內存利用不足(因為可能會分配一個更大的塊)或是分配的失敗(如果已經沒有可選的塊存在了)。
3,這個算法會使一個內存的分配“栓”住另外一個,導致glibc不能將釋放的內存返回給系統。想象內存中已被分配的兩個塊,塊A和塊B。塊A正好處在中斷點的位置,塊B剛好在A的下面,就算釋放了B,在A被釋放前,glibc也不能相應的調整中斷點。
Glibc並不是一直在試圖將空間返回給系統。通常來說,在每次釋放後堆並不收縮。glibc會維護釋放的內存以供之後的分配使用。只有當堆明顯的大於已分配的內存時,glibc才會減小數據段的大小。

對於較大的分配,glibc並不使用堆而是創建一個匿名內存映射(anonymous memory mapping)來滿足要求。匿名存儲器映射和在第四章討論的基於文件的映射很相似,只是它並不基於文件——所以稱之為“匿名”。實際上,一個匿名內存映射只是一塊已經用0初始化的大的內存塊,以供用戶使用。可以把它想成為單獨為某次分配而使用的堆。因為這種映射的存儲不是基於堆的,所以並不會在數據段內產生碎片。
使用匿名映射來分配內存有下列好處:
1,無需關心碎片。
2,匿名存儲映射的大小的是可調整的,可以設置權限,還能像普通的映射一樣接受建議。
3,每個分配存在於獨立的內存映射。沒有必要再去管理一個全局的堆了。

使用匿名映射與堆比起來也有兩個缺點:
1,每個存儲器映射都是頁面大小的整數倍。可能浪費內存空間。
2,創建一個新的內存映射比從堆中返回內存的負載要大,因為使用堆幾乎不涉及任何內核操作。越小的分配,這樣的問題也越明顯,不涉及小而頻繁的請求。

根據各自的優缺點來判斷,glibc的malloc()使用數據段來滿足小的分配,而匿名內存映射則用來滿足大的分配。兩者的臨界點是可調的(請參閱本章稍後的高級內存分配部分),並會隨著glibc版本的不同而有所變化。目前,臨界點一般是128KB:比128KB小的分配由堆實現,相應地,較大的由匿名存儲器映射來實現。用下面系統調用創建和銷毀系統調用:
void mmap (void start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap (void *start, size_t length);

當輸入的fd是-1時,創建匿名映射,在BSD系統上也可以是打開/dev/zero設備的fd。

高級存儲器分配
int mallopt (int param, int value);
存儲分配操作都是受內核的參數所控制和限制的,程序員可以修改這些參數。比如最大的存儲器映射數量、使用匿名映射還是數據段的判斷閾值、高速內存區域的大小、填充字節數。

size_t malloc_usable_size(void *ptr);
查詢一塊已分配內存中有多少可用字節。

int malloc_trim(size_t padding);
調用malloc_trim()成功時,強制glibc歸還所有的可釋放的動態內存給內核。數據段會盡可能地收縮,但是填充字節被保留下來。然後返回1。失敗時,返回0。

調試內存分配
因為僅僅一個環境變量就能控制調試,你不必重新編譯你的程序。例如,你可以簡單的執行如下指令:
$MALLOCCHECK=1 ./rudder
如果設置為0,存儲系統會忽略所有錯誤。如果它被設為1了,信息會被輸出到標準錯誤輸出stderr。如果設置為2,進程會立即通過abort()終止。

struct mallinfo mallinfo (void);
C標準庫獲得關於動態存儲分配系統的統計數據,包括空閑塊的個數,匿名映射的大小,可用的塊大小等等。

基於棧的分配
void * alloca (size_t size);
在一個棧中實現動態內存分配,不必釋放分配到的內存,失敗就表明出現的棧溢出。但需要註意:
1,如果要讓代碼具有可移植性,你要避免使用alloca()。
2,不能使用由alloca()得到的內存來作為一個函數調用的參數,因為分配到的內存塊會被當做參數保存在函數的棧中。
在Linux系統上,alloca()卻是一個非常好用但沒有被人們認識到的工具。它表現的異常出色(在各種架構下,通過alloca()進行內存分配就和增加棧指針一樣簡單),比malloc()的性能要好很多。對於Linux下較小的內存分配,alloca()能收獲讓人激動的性能。

alloca()常見的用法是用來臨時復制一個字符串,因為這種需求非常多以及alloca()實現的高效,Linux系統專門提供了strdup()來將一個給定的字符串復制到棧中。

C99引進了變長數組(VLAs),變長數組的長度是在運行時決定的,而不是在編譯的時候。alloca()和變長數組的主要區別在於通過前者獲得的內存在函數執行過程中始終存在,而通過後者獲得的內存在出了作用域後便釋放了。這樣的方式有好有壞。在for循環中,我們希望每次循環都能釋放空間以在沒有任何副作用的情況下減小內存的開銷(我們不會希望有多余的內存始終被占用著)。然而,如果出於某種原因我們希望這塊空間能保留到下一輪的循環中,那麽使用alloca()顯然是更加合理的。

選擇一個合適的內存分配機制
分配方式 優點 缺點
malloc() 簡單,方便,最常用 返回的內存為用零初始化
calloc() 使數組分配變得容易,用0初始化了內存 在分配非數組空間時顯得較復雜
realloc() 調整已分配的空間大小 只能用來調整已分配空間的大小
brk()和sbrk() 允許對堆進行深入控制 對大多數使用者來說過於底層
匿名內存映射 使用簡單,可共享,允許開發者調整保護等級並提供建議,適合大空間的分配 不適合小分配。最優時malloc()會自動使用匿名內存映射
posix_memalign() 分配的內存按照任何合理的大小進行對齊 相對較新,因此可移植性是一個問題;對於對齊的要求不是很迫切的時候,則沒有必要使用
memalign()和valloc() 相比posix_memalign()在其它的Unix系統上更常見 不是POSIX標準,對對齊的控制能力不如posix_memalign()
alloca() 最快的分配方式,不需要知道確切的大小,對於小的分配非常適合 不能返回錯誤信息,不適合大分配,在一些Unix系統上表現不好
變長數組 與alloca()類似,但在退出此層循環時釋放空間,而不是函數返回時 只能用來分配數組,在一些情況下alloca()的釋放方式更加適用,在其它Unix系統中沒有alloca()常見

存儲器操作
void memset (void s, int c , size_t n);
int memcmp (const void s1, const void s 2 , size_t n);
void memmove (void dst, const void src, size_t n);
void
memcpy (void dst, const void src, size_t n);
void memchr (const void s, int c, size_t n);
void memfrob (void s, size_t n);
C語言提供了很多函數進行內存操作。這些函數的功能和字符串操作函數(如strcmp()以及strcpy())類似,但是他們處理的對象是用戶提供的內存區域而不是以NULL結尾的字符串。要註意這些函數都不會返回錯誤信息。因此防範錯誤是程序員的責任,如果傳遞錯誤的內存區域作參數的話,你將毫無疑問的得到段錯誤。

內存鎖定
Linux實現了請求頁面調度,頁面調度是說在需要時將頁面從硬盤交換進來,當不再需要時再交換出去。這使得系統中進程的虛擬地址空間與實際的物理內存大小沒有直接的關系,同時硬盤上的交換空間提供一個擁有近乎無限物理內存的假象,在下面兩種情況下,應用程序可能希望影響系統的頁面調度:
1,確定性(Determinism) 時間約束嚴格的應用程序需要自己來決定頁的調度行為。如果一些內存操作引起了頁錯誤——這會導致昂貴的磁盤操作——應用程序則可能會超出要求的運行時間。如果能確保需要的頁面總在內存中且從不被交換進磁盤,應用程序就能保證內存操作不會導致頁錯誤,提供一致的,可確定的程序行為,從而提供了效能。
2,安全性(Security) 如果內存中含有私人信息,這些信息可能最終被頁面調度以不加密的方式儲存到硬盤上。例如,如果一個用戶的私鑰正常情況下是以加密的方式保存在磁盤上的,一個在內存中未加密的密鑰備份最後可能保存在了交換文件中。在一個高度註重安全性的環境中,這樣做可能是不可接受。這樣的應用程序可以請求將密鑰一直保留在物理內存上。

int mlock (const void addr, size_t len);
int mlockall (int flags);
int munlock (const void
addr, size_t len);
int munlockall (void);
int mincore (void start, size_t length, unsigned char vec);
因為內存的鎖定能影響一個系統的整體性能-實際上,如果太多的頁面被鎖定,內存分配會失敗——Linux對於一個進程能鎖定的頁面數進行了限制。擁有CAP_IPC_LOCK權限的進程能鎖定任意多的頁面。沒有這個權限的進程只能鎖定RLIMIT_MEMLOCK個字節。

投機性存儲分配策略
Linux使用投機分配策略。當一個進程向內核請求額外的內存——如擴大它的數據段,或者創建一個新的存儲器映射——內核作出了分配承諾但實際上並沒有分給進程任何的物理存儲。僅當進程對新“分配到”的內存區域作寫操作的時候,內核才履行承諾,分配一塊物理內存。內核逐頁完成上述工作,並在需要時進行請求頁面調度和寫時復制。這麽做的優點:
1,延緩內存分配允許內核將大部分工作推遲到最後一刻(當確實需要進行分配時)。
2,由於請求是根據需求逐頁的分配,只有真正需要物理內存的時候才會消耗物理存儲。
3,分配到的虛擬內存可能比實際的物理內存甚至比可用的交換空間多得多。這個特征叫做超量使用(overcommitment)。

超量使用的功能可以通過修改配置文件 /proc/sys/vm/overcommit_memory來關閉。如果設置為2,則是使用嚴格審計(strict accounting)策略,將虛擬內存限定在物理內存的一定比例之內。默認是50,因為物理內存還需要包含內核、頁表、系統保留頁,鎖定頁等。

Linux 內存管理