Linux應用開發之內存分配

2023-05-08 11:27:06 來源:TLPI系統編程筆記

1、在堆上分配內存

堆是長度可變的連續虛擬內存,始于進程未初始化數據段的末尾,將堆當前的內存邊界稱為 "programbreak"。

1.1、調整 program break


(資料圖)

改變堆的大小,其實就像命令內核改變進程的 program break 位置一樣,最初,program break 的位置正好位于未初始化數據段末尾之后。

#includeintbrk(void*end_data_segment);void*sbrk(intptr_tincrement);

brk()會將 program break 設置為參數 end_data_segment 所指定的位置,由于虛擬內存以頁為分配單位,end_data_segment 實際會四舍五入到下一個內存頁的邊界處:

試圖將 program break 設置為一個低于其初始值的位置時,有可能導致無法預知的行為

program break 可以設置的額精確上限取決于一系列的因素,包括進程中對數據段大小的資源限制,以及內存映射、共享內存段、共享庫的位置

sbrk()將 program break 在原有地址上增加了從參數increment傳入的大小,如果調用成功sbrk()返回前一個 program break 的地址,也就是說如果 program break 增加,那么返回值將是指向這塊新分配內存起始位置的指針

sbrk(0)將返回 program break 的當前位置,對其不做改變

在 program break 的位置提升之后,程序可以訪問新分配區域內的任何內存地址,而此時物理內存頁尚未分配,內核會在進程首次視圖訪問這些虛擬內存地址時自動分配新的物理內存頁。

1.2、在堆上分配內存

#includevoid*malloc(size_tsize);

malloc()在堆上分配size個字節大小的內存,并返回指向新分配內存起始位置處的指針,其分配的內存未經初始化

malloc返回的內存塊采用了字節對齊方式,一般是基于 8 或者 16 字節邊界來進行內存分配,從而能夠高效地訪問任何類型的 C 語言數據結構

如果無法分配內存,malloc()將會返回NULL,并設置errno,雖然內存分配失敗的可能性很小,但是還是應該對malloc()返回值進行檢查

#includevoidfree(void*ptr);

free()函數釋放ptr所指向的內存塊

一般情況下,free()并不降低 program break 的位置,而是將這塊內存添加到空閑內存列表中,供后續的malloc()函數循環使用:

被釋放的內存塊通常位于堆的中間,而非堆的頂部,因而降低 program break 是不可能的

最大限度地減少了程序必須執行的sbrk()調用次數,從而降低系統開銷

大多數情況下,降低 program break 的位置不會對那些分配大量內存的程序有多少幫助,因為它們通常傾向于持有已分配內存或者是反復釋放和重新分配內存

給free()傳遞一個NULL指針,那么函數將什么都不做

調用free()后對參數的ptr的任何使用,包括重新傳遞給free()將產生不可預知的結果

1.3、調用free()還是不調用free()

進程終止時,其占用的所有內存都會返還給操作系統,這包括在堆內存中由malloc()函數包內一系列函數所分配的內存。

雖然依靠終止進程來自動釋放內存對大多數程序來說是可接受的,但是基于以下原因,最好能夠在程序中顯式釋放所有分配的內存:

顯示調用free()能使程序在未來修改時,更具可讀性和可維護性

如果使用malloc()調試庫來查找內存泄漏,那么會將任何未經顯式釋放處理的內存報告為內存泄漏,這會使分析變得復雜

1.4、malloc()和free()的實現

malloc()的實現很簡單:

首先會掃描之前由free()所釋放的空閑內存塊列表,以求找到尺寸大于或等于要求的一塊空閑內存,采用的掃描策略可能有 first-fit 或者 best-fito

如果這一內存塊的尺寸正好與要求相當,就把它直接返回給調用者,如果是一塊比較大的內存,那么將對其進行分割,再將一塊大小相當的內存返回給調用者的同時,把剩余的那塊內存塊保留在空閑列表中

如果在空閑列表中根本找不到足夠大的空閑內存塊,那么malloc()將調用sbrk()以分配更多的內存,為了減少對sbrk()的調用次數,malloc()并未只是嚴格按所需的字節數來分配內存,而是以更大幅度(以虛擬內存頁大小的整數倍) 來增加 program break,并將超出部分置于空閑內存列表

malloc()分配內存時會多分配幾個字節用來記錄這塊內存的大小整數值,這個整數位于內存塊的起始處,而實際返回給調用者的內存地址恰好位于這一長度記錄字節之后。

free()的實現更為有趣:

free()將內存塊置于空閑列表之上

歸還的大小正是依據malloc()預留的整數值

當將內存塊置于空閑內存列表(雙向鏈表)時,free()會使用內存塊本身的空間來存放鏈表指針,將自身添加到列表中:

隨著對內存不斷地釋放和重新分配,空閑列表中的空閑內存會和已經分配的在用內存混雜在一起:

避免內存分配相關問題,應該遵循的準則:

分配一塊內存后,不要改變這塊內存范圍外的任何內容

釋放同一塊內存超過一次是錯誤的,結果是不可預知的

不是經由malloc函數包中函數返回的指針,決不能在調用free()函數時使用

避免內存泄漏

1.5、malloc 調試的工具和庫

glibc 提供的 malloc 調試工具:

mtrace()和muntrace()函數分別在程序打開和關閉對內存分配調用進行跟蹤的功能。這些函數要與環境變量MALLOC_TRACE搭配使用,該變量定義了寫入跟蹤信息的文件名

mcheck()和mprobe()函數允許對已分配內存塊進行一致性檢查

MALLOC_CHECK_環境變量提供了mcheck()和mprobe()函數的功能,區別在于MALLOC_CHECK_無需對程序進行修改和重新編譯,將此變量設置為不同的整數值,可以控制程序對內存分配錯誤的響應方式:

0 :忽略錯誤

1 :在標準錯誤輸出診斷錯誤

調用abort()來終止程序

1.6、控制和監控 malloc 函數包

glibc 手冊介紹了一系列非標準函數,可以用于監測和控制 malloc 包中的函數:

mallopy()能修改各項參數,以控制malloc()所采用的算法

mallinfo()返回一個結構,其中包含由malloc()分配內存的各種統計數據

堆上分配內存的其他方法

用calloc()和realloc()分配內存

#includevoid*calloc(size_tnumitems,size_tsize);

numitems指定分配對象的數量,size指定每個對象的大小

分配成功返回這塊內存起始處的指針,無法分配時返回NULL

calloc()會將已分配的內存初始化為 0

#includevoid*realloc(void*ptr,size_tsize);

realloc()用來調整(通常是增加)一塊內存的大小,此塊內存應該是之前由malloc包中函數所分配的

ptr是指向需要調整大小的內存塊的指針,size指定所需調整大小的期望值

成功時realloc()返回指向大小調整后內存塊的指針,與調用之前的指針相比,兩者可能不同,如果發生錯誤,realloc()返回NULL,對ptr指針指向的內存塊則保持不變

realloc()不會對額外分配的字節進行初始化

調用realloc(ptr,0)等效于free(ptr)之后再調用malloc(0),調用realloc(NULL,size)相當于調用malloc(size)

通常情況下,當增大已分配內存時:

realloc()會試圖去合并在空閑列表中緊跟其后其大小滿足要求的內存塊

如果不存在,并且原內存塊位于堆的頂部,那么realloc()將會對堆空間進行擴展,如果原來的內存塊在堆的中部,且緊鄰其后的空間不足,realloc()會分配一塊新的內存,并且將原有的數據復制到新的內存,這種形式更為常見,會占用大量的 CPU資源

由于realloc()可能會移動內存,對這塊內存的后續引用就必須使用realloc()返回的指針

一般應該盡量避免使用realloc()

1.7、分配對齊的內存

#includevoid*memalign(size_tboundary,size_tsize);

起始地址是boundary的整數倍,boundary必須是 2 的整數次冪

memalign()并非在所有的 UNIX 實現上都存在,大多數memalign()的其他 UNIX 實現要求引用

#includeintposix_memalign(void**memptr,size_talignment,size_tsize);

posix_memalign()只在少數 UNIX 實現,與memalign()有兩個方面不同:

已分配的內存地址通過memptr返回

內存與alignment參數的整數倍對齊,alignment必須是sizeof(void*)與 2 的整數次冪兩者之間的乘積

出錯時不返回 -1,而是直接返回一個錯誤號

2、在堆棧上分配內存

#includevoid*alloca(size_tsize);

alloca()通過增加棧幀的大小從堆棧上分配內存

不能也不需要調用free()來釋放alloca()分配的內存

如果調用alloca()造成堆棧溢出,則程序的行為是無法預知的

不要在參數列表中調用alloca(),這會使得分配的堆棧空間出現在當前函數參數的空間內:

func(x,alloca(size),z)//@錯誤的示范//@必須按下面的方式進行void*y;y=alloca(size);func(x,y,z);

使用alloca()來分配內存相對于malloc()有一定優勢:

alloca()分配內存的速度要快于malloc(),因為編譯器將alloca()作為內聯代碼處理,而是通過直接調整堆棧指針來實現的

alloca()不需要維護空閑內存塊列表

alloca()分配的內存隨著棧幀的移除會自動釋放,亦即當調用alloca()的函數返回之時,因為函數返回時所執行的代碼會重置棧指針寄存器,使其指向前一幀的末尾

信號處理程序中調用longjump()或siglongjump()以執行非局部跳轉時,alloca()的作用尤其突出,此時在 "起跳" 和 "落地" 之間的函數如果使用malloc()則很難避免內存泄漏

審核編輯:湯梓紅

標簽:

上一篇:MOS管燒壞常見的可能性故障分析
下一篇:最后一頁