系統(tǒng)調(diào)用與內(nèi)存管理(sbrk、brk、mmap、munmap)

一、系統(tǒng)調(diào)用(System Call):

在Linux中,4G內(nèi)存可分為兩部分——內(nèi)核空間1G(3 ~ 4G)與用戶空間3G(0 ~ 3G),我們通常寫的C代碼都是在對用戶空間即0 ~ 3G的內(nèi)存進行操作。而且,用戶空間的代碼不能直接訪問內(nèi)核空間,因此內(nèi)核空間提供了一系列的函數(shù),實現(xiàn)用戶空間進入內(nèi)核空間的接口,這一系列的函數(shù)稱為系統(tǒng)調(diào)用(System Call)。比如我們經(jīng)常使用的open、close、read、write等函數(shù)都是系統(tǒng)級別的函數(shù)(man 2 function_name),而像fopen、fclose、fread、fwrite等都是用戶級別的函數(shù)(man 3 function_name)。不同級別的函數(shù)能夠操作的內(nèi)存區(qū)域自然也就不同。

我們用一幅圖來描述函數(shù)的調(diào)用過程:

對于C++中new與delete的底層則是用malloc和free實現(xiàn)。而我們所用的malloc()、free()與內(nèi)核之間的接口(橋梁)就是sbrk()等系統(tǒng)函數(shù);當然我們也可以直接調(diào)用系統(tǒng)調(diào)用(系統(tǒng)函數(shù)),達到同樣的作用。我們可以用下面這幅圖來描述基本內(nèi)存相關(guān)操作之間的關(guān)系:

雖然使用系統(tǒng)調(diào)用會帶來一定的好處,但是物極必反,系統(tǒng)調(diào)用并非能頻繁使用。由于程序由用戶進入內(nèi)核層時,會將用戶層的狀態(tài)先封存起來,然后到內(nèi)核層運行代碼,運行結(jié)束以后,從內(nèi)核層出來到用戶層時,再把數(shù)據(jù)加載回來。因此,頻繁的系統(tǒng)調(diào)用效率很低。今天我們就系統(tǒng)調(diào)用層面來對內(nèi)存操作做進一步的了解。

二、內(nèi)存管理(Memory Management)系統(tǒng)調(diào)用:

1、brk()與sbrk():

(1)、函數(shù)原型與實現(xiàn):

//函數(shù)原型:
#include<unistd.h>
int brk(void * addr); 
void * sbrk(intptr_t increment);

由于sbrk()與brk()這兩個系統(tǒng)函數(shù)有點所謂怪異,我們先來看看man手冊對于sbrk()與brk()的描述:

DESCRIPTION

brk() and sbrk() change the location of the program break, which
defines the end of the process's data segment (i.e., the program
break is the first location after the end of the uninitialized data
segment). Increasing the program break has the effect of allocating
memory to the process; decreasing the break deallocates memory.

brk() sets the end of the data segment to the value specified by
addr, when that value is reasonable, the system has enough memory,
and the process does not exceed its maximum data size (see
setrlimit(2)).

sbrk() increments the program's data space by increment bytes.
Calling sbrk() with an increment of 0 can be used to find the current
location of the program break.

RETURN VALUE

On success, brk() returns zero. On error, -1 is returned, and errno
is set to ENOMEM.

On success, sbrk() returns the previous program break. (If the break
was increased, then this value is a pointer to the start of the newly
allocated memory). On error, (void *) -1 is returned, and errno is
set to ENOMEM.

描述:
brk()和sbrk()改變程序間斷點的位置。程序間斷點就是程序數(shù)據(jù)段的結(jié)尾。(程序間斷點是為初始化數(shù)據(jù)段的起始位置).通過增加程序間斷點進程可以更有效的申請內(nèi)存 。當addr參數(shù)合理、系統(tǒng)有足夠的內(nèi)存并且不超過最大值時brk()函數(shù)將數(shù)據(jù)段結(jié)尾設置為addr,即間斷點設置為addr。sbrk()將程序數(shù)據(jù)空間增加increment字節(jié)。當increment為0時則返回程序間斷點的當前位置。

返回值:
brk()成功返回0,失敗返回-1并且設置errno值為ENOMEM(注:在mmap中會提到)。
sbrk()成功返回之前的程序間斷點地址。如果間斷點值增加,那么這個指針(指的是返回的之前的間斷點地址)是指向分配的新的內(nèi)存的首地址。如果出錯失敗,就返回一個指針并設置errno全局變量的值為ENOMEM。

總結(jié):
這兩個函數(shù)都用來改變 “program break” (程序間斷點)的位置,改變數(shù)據(jù)段長度(Change data segment size),實現(xiàn)虛擬內(nèi)存到物理內(nèi)存的映射。
brk()函數(shù)直接修改有效訪問范圍的末尾地址實現(xiàn)分配與回收。sbrk()參數(shù)函數(shù)中:當increment為正值時,間斷點位置向后移動increment字節(jié)。同時返回移動之前的位置,相當于分配內(nèi)存。當increment為負值時,位置向前移動increment字節(jié),相當與于釋放內(nèi)存,其返回值沒有實際意義。當increment為0時,不移動位置只返回當前位置。參數(shù)increment的符號決定了是分配還是回收內(nèi)存。而關(guān)于program break的位置如圖所示:

(2)、簡單測試:

對于分配好的內(nèi)存,我們只要有其首地址old與長度MAX*MAX即可不越界的準確使用(如下圖所示),其效果與malloc相同,只不過sbrk()與brk()是C標準函數(shù)的底層實現(xiàn)而已,其機制較為復雜(測試中,死循環(huán)是為了查看maps文件,不至于進程消亡文件隨之消失)。

雖然,sbrk()與brk()均可分配回收兼職,但是我們一般用sbrk()分配內(nèi)存,而用brk()回收內(nèi)存,上例中回收內(nèi)存可以這樣寫:

int err = brk(old);
// 或者brk(p);效果與sbrk(-MAX*MAX);是一樣的,但brk()更方便與清晰明了。
if(-1 == err){
    perror("brk");
    exit(EXIT_FAILURE);
}

2、mmap()與munmap():

mmap函數(shù)(地址映射):mmap將一個文件或者其它對象映射進內(nèi)存。文件被映射到多個頁上,如果文件的大小不是所有頁的大小之和,最后一個頁不被使用的空間將會清零(Linux堆空間未使用內(nèi)存均清零)。這里我們只研究mmap的內(nèi)存映射,而暫時不討論文件方面的問題。關(guān)于mmap的文件映射的更詳細的內(nèi)容可參考:認真分析mmap:是什么 為什么 怎么用

//函數(shù)原型:
#incldue<sys/mman.h>
void * mmap(void * addr, size_t length,int prot,int flags,int fd,off_t offset);

參數(shù):

(1)、addr:
起始地址,置零讓系統(tǒng)自行選擇并返回即可。
(2)、length:
長度,不夠一頁會自動湊夠一頁的整數(shù)倍,我們可以宏定義#define MIN_LENGTH_MMAP 4096為一頁大小。
(3)、prot:
讀寫操作權(quán)限,PROT_READ可讀、PROT_WRITE可寫、PROT_EXEC可執(zhí)行、PROT_NONE映射區(qū)域不能讀取。(注意PROT_XXXXX與文件本身的權(quán)限不沖突,如果在程序中不設定任何權(quán)限,即使本身存在讀寫權(quán)限,該進程也不能對其操作)。
(4)、flags常用標志:
MAP_SHARED【share this mapping】、MAP_PRIVATE【Create a private copy-on-write mapping】
MAP_SHARED只能設置文件共享,不能地址共享,即使設置了共享,對于兩個進程來說,也不會生效。而MAP_PRIVATE則對于文件與內(nèi)存都可以設置為私有。
MAP_ANON【Deprecated】、MAP_ANONYMOUS:匿名映射,如果映射地址需要加該參數(shù),如果不加默認映射文件。MAP_ANON已經(jīng)過時,只需使用MAP_ANONYMOUS即可。
(5)、fd:文件描述符。
(6)、offset:文件描述符偏移量
(fd和offset對于一般性內(nèi)存分配來說設置為0即可)

返回值:

失敗返回MAP_FAILED,即(void * (-1))并設置errno全局變量。
成功返回指向mmap area的指針pointer。

常見errno錯誤:

ENOMEM:內(nèi)存不足;
EAGAIN:文件被鎖住或有太多內(nèi)存被鎖??;
EBADF:參數(shù)fd不是有效的文件描述符;
EACCES:存在權(quán)限錯誤,。如果是MAP_PRIVATE情況下文件必須可讀;使用MAP_SHARED則文件必須能寫入,且設置prot權(quán)限必須為PROT_WRITE。
EINVAL:參數(shù)addr、length或者offset中有不合法參數(shù)存在。

munmap函數(shù):解除映射關(guān)系

// addr為mmap函數(shù)返回接收的地址,length為請求分配的長度。
int munmap(void * addr, size_t length);

這張圖描述了mmap內(nèi)存地址映射的位置關(guān)系(棧區(qū)以上為內(nèi)核空間)。關(guān)于這一點我們可以作以簡單的測試(我采用MIN_LENGTH_MMAP宏,當然你也可以用多少申請多少,系統(tǒng)總是以最小1頁來映射的,關(guān)于內(nèi)存分頁與虛擬地址映射可參考:Linux系統(tǒng)內(nèi)存管理與內(nèi)存分頁機制

mmap映射的地址處于堆區(qū)與棧區(qū)中間,malloc映射的堆區(qū)內(nèi)存為33頁(最小映射大?。鴐map映射的內(nèi)存為3頁,也是4096的整數(shù)倍。

作者:Apollon_krj
鏈接:https://blog.csdn.net/Apollon_krj/article/details/54565768
來源:CSDN
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容