歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux基礎知識 >> UNIX高級環境編程(15)進程和內存分配 < 故宮角樓 >

UNIX高級環境編程(15)進程和內存分配 < 故宮角樓 >

日期:2017/3/2 17:15:03   编辑:Linux基礎知識

故宮角樓是很多攝影愛好者常去的地方,夕陽余輝下的故宮角樓平靜而安詳。

故宮角樓

?

首先,了解一下進程的基本概念,進程在內存中布局和內容。

此外,還需要知道運行時是如何為動態數據結構(如鏈表和二叉樹)分配額外內存的。

一 進程

1 進程和程序

進程:是一個可執行程序的實例。

程序:包含一系列信息的文件,這些信息描述了如何在運行時創建一個進程。包含如下信息:

  1. 二進制格式標識:如最常見的ELF格式。
  2. 機器語言指令:對程序算法進行編碼。
  3. 程序入口地址:標識程序開始執行時的起始指令位置。
  4. 數據:程序文件包含的變量初始值和程序使用的字面常量值,如字符串。
  5. 符號表和重定位表:描述程序中函數和變量的位置及名稱。
  6. 共享庫和動態鏈接信息:程序文件中所包含的一些字段,列出了程序運行時需要使用的共享庫,以及加載共享庫的動態鏈接器的路徑名。
  7. 其他信息。

進程的再定義:進程是由內核定義的抽象的實體,並為該實體分配用以執行程序的各項系統資源。

從內核的角度看,進程由用戶內存空間和一系列內核數據結構組成,其中用戶內存空間包含了程序代碼及代碼所使用的變量,而內核數據結構則用於維護進程狀態信息。

2 典型的進程內存布局

NewImage

每個進程所分配的內存由很多部分組成,通常稱之為“段(segment)”。如上圖所示:

  1. 文本段:包含進程運行的程序機器語言指令。文本段具有只讀屬性,因此多個進程可同時運行同一程序,共享文本段。
  2. 初始化數據段:包含顯式初始化的全局變量和靜態變量。當程序加載到內存時,從可執行文件中讀取這些變量的值。
  3. 未初始化數據段(BSS段,block started by symbol):包含了未進行顯式初始化的全局變量和靜態變量。程序啟動之前,系統將本段內所有內存初始化為0.所以又叫做零初始化數據段。
  4. 棧(stack):動態增長和收縮的段,由棧幀(stack frame)組成。系統會為每個當前調用的函數分配一個棧幀。棧幀中存儲了函數的局部變量、實參和返回值。
  5. 堆(heap):在運行時為變量動態進行內存分配的一塊區域。堆頂端成為程序中斷(program break)
將經過初始化的全局變量和靜態變量與未經過初始化的全局變量和靜態變量分開存放,其主要原因在於程序在磁盤上存儲時,沒有必要為未經過初始化的變量分配存儲空間。相反,可執行文件只需記錄未初始化數據段的位置及所需要大小,直到運行時再由程序加載器來分配這一空間。

需要注意一點時,該內存布局的討論是在虛擬內存中的,並不是物理內存中的布局。

在後面會專門討論虛擬內存的一些細節。

?

二 內存分配

1 在堆上分配內存

堆:一段長度可變的連續虛擬內存,始於進程的未初始化數據段末尾,隨著內存的分配和釋放而增減。將堆的當前內存頂部邊界稱為“程序中斷(program break)”

program break是一個非常重要的概念,因為分配和釋放內存的實際動作就是改變進程的program break位置。

program break的起始位置(堆的大小為0)位於未初始化數據段末尾之後。

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

函數malloc和free

malloc函數聲明

#include
void *malloc(size_t size); 

作用:在堆上分配參數size字節大小的內存。

返回值:成功返回指向新分配內存起始地址的指針,失敗返回NULL

free函數聲明?

#include
void free(void *ptr);

?作用:釋放ptr參數所指向的內存塊,該參數應該是之前由malloc或者其他內存分配函數之一所返回的地址。

需要注意的是:一般情況下,free並不降低program break的位置,而是將這塊內存增加到空閒內存列表中,供後續的malloc函數循環使用。因為:

  • 被釋放的內存塊通常位於堆的中間,而非堆的頂部,因而降低program break是不可能的。
  • 它最大限度地減少了內核調用調整program break系統調用的次數。
  • 通常程序會持有分配的內存或者反復釋放和重新分配,而不是釋放所有內存再運行一段時間。

僅當堆頂空閒內存“足夠”大的時候,free函數的glibc實現會調用sbrk()來降低program break的地址,至於“足夠”與否則取決於malloc函數包行為的控制參數(128KB為典型值)。這減少了必須對sbrk()發起的調用次數。

malloc和free的實現

malloc()的實現

  1. 掃描之前由free()所釋放的空閒內存塊列表,以求找到尺寸大於或者等於要求的一塊內存
  2. 如果這一內存塊的尺寸正好與要求相當,就把它直接返回給調用者。
  3. 如果是一塊較大的內存,那麼將對其進行分割,在將一塊大小相當的內存返回給調用者的同時,把較小的那塊空閒內存塊保留在空閒列表。
  4. 如果在空閒內存列表中找不到足夠大的空閒內存塊,那麼malloc會調用sbrk()以分配更多的內存,並且malloc會分配出比所需字節數更多的內存,將超出的部分置於空閒內存列表中。

free()的實現

首先先了解兩點:malloc返回的內存塊和空閒列表中的內存塊的結構

為了知道每一個內存塊的大小,當malloc分配內存塊時,會額外分配幾個字節來存放記錄這塊內存大小的整數值。該整數位於內存塊的起始處,而實際返回給調用者的內存地址恰好位於這一長度記錄字節之後。如下圖所示:

NewImage

為了管理空閒內存列表,free()會使用內存塊本身的空間來存放鏈表指針,將自身添加到列表中。如下圖所示:

NewImage

所以,在頻繁地分配和釋放內存之後,堆中的鏈表可能會變成下圖的樣子,空閒鏈表中的空閒內存會和已分配的在用內存混雜在一起。

NewImage

?

三 編程需要注意的事項

通過對內存相關知識更多的了解,在平時編程的時候,應更清楚為什麼我們需要遵守下面的規則。

  1. 分配一塊內存後,不要改變這塊內存范圍外的任何內容。
  2. 釋放同一塊已分配內存超過一次是錯誤的。當兩次釋放同一塊內存時,常見的後果是導致不可預知的行為。
  3. 若非經由malloc函數包中函數所返回的指針,絕不能在調用free()函數使用。
  4. 如果需要反復分配內存,那麼應當確保釋放所有已使用完畢的內存,不然將導致內存洩露。
?
雖然在我們平時的工作當中,可能涉及不到這麼底層的原理,但是通過對這些基本原理的了解,可以讓我們更加清除,我們寫代碼究竟在寫些什麼 :)

?

參考資料:

《Linux/Unix系統編程手冊(上冊)》 第6章,第7章?

Copyright © Linux教程網 All Rights Reserved