Linux 應用開發之內存分配
來源 | TLPI 系統編程筆記
整理 & 排版 | 嵌入式應用研究院
1、在堆上分配內存
堆是長度可變的連續虛擬內存,始於進程未初始化數據段的末尾,將堆當前的內存邊界稱爲 "program break"。
1.1、調整 program break
改變堆的大小,其實就像命令內核改變進程的 program break 位置一樣,最初,program break 的位置正好位於未初始化數據段末尾之後。
#include <unistd.h>
int brk(void *end_data_segment);
void *sbrk(intptr_t increment);
-
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、在堆上分配內存
#include <stdlib.h>
void *malloc(size_t size);
-
malloc()
在堆上分配size
個字節大小的內存,並返回指向新分配內存起始位置處的指針,其分配的內存未經初始化 -
malloc
返回的內存塊採用了字節對齊方式,一般是基於 8 或者 16 字節邊界來進行內存分配,從而能夠高效地訪問任何類型的 C 語言數據結構 -
如果無法分配內存,
malloc()
將會返回NULL
,並設置errno
,雖然內存分配失敗的可能性很小,但是還是應該對malloc()
返回值進行檢查
#include <stdlib.h>
void free(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()
分配內存
#include <stdlib.h>
void *calloc(size_t numitems, size_t size);
-
numitems
指定分配對象的數量,size
指定每個對象的大小 -
分配成功返回這塊內存起始處的指針,無法分配時返回
NULL
-
calloc()
會將已分配的內存初始化爲 0
#include <stdlib.h>
void *realloc(void *ptr, size_t size);
-
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、分配對齊的內存
#include <malloc.h>
void *memalign(size_t boundary, size_t size);
-
起始地址是
boundary
的整數倍,boundary
必須是 2 的整數次冪 -
memalign()
並非在所有的 UNIX 實現上都存在,大多數memalign()
的其他 UNIX 實現要求引用<stdlib.h>
#include <stdlib.h>
int posix_memalign(void **memptr, size_t alignment, size_t size);
-
posix_memalign()
只在少數 UNIX 實現,與memalign()
有兩個方面不同: -
已分配的內存地址通過
memptr
返回 -
內存與
alignment
參數的整數倍對齊,alignment
必須是sizeof(void*)
與 2 的整數次冪兩者之間的乘積 -
出錯時不返回 -1,而是直接返回一個錯誤號
2、在堆棧上分配內存
#include <alloca.h>
void *alloca(size_t size);
-
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()
則很難避免內存泄漏
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ikVEzOIC9pUQNdUOXXqqnw