1. 程式人生 > >tuolaji8的專欄

tuolaji8的專欄

程序如何使用記憶體?

毫無疑問,所有程序(執行的程式)都必須佔用一定數量的記憶體,它或是用來存放從磁碟載入的程式程式碼,或是存放取自使用者輸入的資料等等。不過程序對這些記憶體的管理方式因記憶體用途不一而不盡相同,有些記憶體是事先靜態分配和統一回收的,而有些卻是按需要動態分配和回收的。

對任何一個普通程序來講,它都會涉及到5種不同的資料段。稍有程式設計知識的朋友都能想到這幾個資料段中包含有“程式程式碼段”、“程式資料段”、“程式堆疊段”等。不錯,這幾種資料段都在其中,但除了以上幾種資料段之外,程序還另外包含兩種資料段。下面我們來簡單歸納一下程序對應的記憶體空間中所包含的5種不同的資料區。

程式碼段

:程式碼段是用來存放可執行檔案的操作指令,也就是說是它是可執行程式在記憶體中的映象。程式碼段需要防止在執行時被非法修改,所以只准許讀取操作,而不允許寫入(修改)操作——它是不可寫的。

資料段:資料段用來存放可執行檔案中已初始化全域性變數,換句話說就是存放程式靜態分配[1]的變數和全域性變數。

BSS段[2]:BSS段包含了程式中未初始化的全域性變數,在記憶體中 bss段全部置零。

堆(heap):堆是用於存放程序執行中被動態分配的記憶體段,它的大小並不固定,可動態擴張或縮減。當程序呼叫malloc等函式分配記憶體時,新分配的記憶體就被動態新增到堆上(堆被擴張);當利用free等函式釋放記憶體時,被釋放的記憶體從堆中被剔除(堆被縮減)

:棧是使用者存放程式臨時建立的區域性變數,也就是說我們函式括弧“{}”中定義的變數(但不包括static宣告的變數,static意味著在資料段中存放變數)。除此以外,在函式被呼叫時,其引數也會被壓入發起呼叫的程序棧中,並且待到呼叫結束後,函式的返回值也會被存放回棧中。由於棧的先進先出特點,所以棧特別方便用來儲存/恢復呼叫現場。從這個意義上講,我們可以把堆疊看成一個寄存、交換臨時資料的記憶體區。

程序如何組織這些區域?

上述幾種記憶體區域中資料段、BSS和堆通常是被連續儲存的——記憶體位置上是連續的,而程式碼段和棧往往會被獨立存放。有趣的是,堆和棧兩個區域關係很“曖昧”,他們一個向下“長”(i386體系結構中棧向下、堆向上),一個向上“長”,相對而生。但你不必擔心他們會碰頭,因為他們之間間隔很大(到底大到多少,你可以從下面的例子程式計算一下),絕少有機會能碰到一起。

下圖簡要描述了程序記憶體區域的分佈:

“事實勝於雄辯”,我們用一個小例子(原形取自《User-Level Memory Management》)來展示上面所講的各種記憶體區的差別與位置。

#include<stdio.h>

#include<malloc.h>

#include<unistd.h>

int bss_var;

int data_var0=1;

int main(int argc,char **argv)

{

  printf("below are addresses of types of process's mem\n");

  printf("Text location:\n");

  printf("\tAddress of main(Code Segment):%p\n",main);

  printf("____________________________\n");

  int stack_var0=2;

  printf("Stack Location:\n");

  printf("\tInitial end of stack:%p\n",&stack_var0);

  int stack_var1=3;

  printf("\tnew end of stack:%p\n",&stack_var1);

  printf("____________________________\n");

  printf("Data Location:\n");

  printf("\tAddress of data_var(Data Segment):%p\n",&data_var0);

  static int data_var1=4;

  printf("\tNew end of data_var(Data Segment):%p\n",&data_var1);

  printf("____________________________\n");

  printf("BSS Location:\n");

  printf("\tAddress of bss_var:%p\n",&bss_var);

  printf("____________________________\n");

  char *b = sbrk((ptrdiff_t)0);

  printf("Heap Location:\n");

  printf("\tInitial end of heap:%p\n",b);

  brk(b+4);

  b=sbrk((ptrdiff_t)0);

  printf("\tNew end of heap:%p\n",b);

return 0;

 }

它的結果如下

below are addresses of types of process's mem

Text location:

   Address of main(Code Segment):0x8048388

____________________________

Stack Location:

   Initial end of stack:0xbffffab4

   new end of stack:0xbffffab0

____________________________

Data Location:

   Address of data_var(Data Segment):0x8049758

   New end of data_var(Data Segment):0x804975c

____________________________

BSS Location:

   Address of bss_var:0x8049864

____________________________

Heap Location:

   Initial end of heap:0x8049868

   New end of heap:0x804986c

利用size命令也可以看到程式的各段大小,比如執行size example會得到

text data bss dec hex filename

1654 280   8 1942 796 example

但這些資料是程式編譯的靜態統計,而上面顯示的是程序執行時的動態值,但兩者是對應的。

程序的記憶體空間:

Linux作業系統採用虛擬記憶體管理技術,使得每個程序都有各自互不干涉的程序地址空間。該空間是塊大小為4G的線性虛擬空間,使用者所看到和接觸到的都是該虛擬地址,無法看到實際的實體記憶體地址。利用這種虛擬地址不但能起到保護作業系統的效果(使用者不能直接訪問實體記憶體),而且更重要的是,使用者程式可使用比實際實體記憶體更大的地址空間 

在討論程序空間細節前,這裡先要澄清下面幾個問題:

l         第一、4G的程序地址空間被人為的分為兩個部分——使用者空間與核心空間。使用者空間從0到3G(0xC0000000),核心空間佔據3G到4G。使用者程序通常情況下只能訪問使用者空間的虛擬地址,不能訪問核心空間虛擬地址。只有使用者程序進行系統呼叫(代表使用者程序在核心態執行)等時刻可以訪問到核心空間。

l         第二、使用者空間對應程序,所以每當程序切換,使用者空間就會跟著變化;而核心空間是由核心負責對映,它並不會跟著程序改變,是固定的。核心空間地址有自己對應的頁表(init_mm.pgd),使用者程序各自有不同的頁表。

l         第三、每個程序的使用者空間都是完全獨立、互不相干的。不信的話,你可以把上面的程式同時執行10次(當然為了同時執行,讓它們在返回前一同睡眠100秒吧),你會看到10個程序佔用的線性地址一模一樣。

程序所能直接操作的地址都為虛擬地址。當程序需要記憶體時,從核心獲得的僅僅是虛擬的記憶體區域,而不是實際的實體地址,程序並沒有獲得實體記憶體,獲得的僅僅是對一個新的線性地址區間的使用權。實際的實體記憶體只有當程序真的去訪問新獲取的虛擬地址時,才會由“請求頁機制”產生“缺頁”異常,從而進入分配實際頁面的例程 。

當應用程式訪問一個虛擬地址時,首先必須將虛擬地址轉化成實體地址,然後處理器才能解析地址訪問請求。地址的轉換工作需要通過查詢頁表才能完成,概括地講,地址轉換需要將虛擬地址分段,使每段虛地址都作為一個索引指向頁表,而頁表項則指向下一級別的頁表或者指向最終的物理頁面。每個程序都有自己的頁表。程序描述符的pgd域指向的就是程序的頁全域性目錄。

實體記憶體管理(頁管理)

實體記憶體是由Linux核心通過分頁機制管理實現的,它將整個記憶體劃分成無數個4k(在i386體系結構中)大小的頁,從而分配和回收記憶體的基本單位便是記憶體頁了。利用分頁管理有助於靈活分配記憶體地址,因為分配時不必要求必須有大塊的連續記憶體,系統可以東一頁、西一頁的湊出所需要的記憶體供程序使用。雖然如此,但是實際上系統使用記憶體時還是傾向於分配連續的記憶體塊,因為分配連續記憶體時,頁表不需要更改,因此能降低TLB的重新整理率(頻繁重新整理會在很大程度上降低訪問速度)。