1. 程式人生 > >【作業系統】堆與記憶體管理概述

【作業系統】堆與記憶體管理概述

轉自: https://blog.csdn.net/bitboss/article/details/70154146

 

—–要說到作業系統的堆與記憶體的管理的話,那內容真的是海了去了,從開始的地方就能不停的擴充套件,但內容的重要性也是不可言喻的,本片部落格著重於總結以下三點:

Linux的虛擬地址空間佈局
堆和棧的管理,堆和棧的區別
中間會涉及到一些擴充套件的知識,但是不會細說!

開始的地方: 程式的記憶體佈局

要說堆和記憶體管理,那麼開始的地方不得不是程式的記憶體佈局,即虛擬地址空間,下面貼出一張虛擬地址空間圖(盜的圖):

分析 : 這張圖可以讓我們很明顯的看明白虛擬地址空間的佈局(32位);

核心部分就不說了,能力不夠 -_-||, 重要的是我們知道虛擬地址空間的高1G的空間是給了核心就可以了,當然這是Linux核心,windows預設給核心分配2G,當然也可以配成1G;(有興趣的同學可以去了解一下中斷,或者程序切換時,核心棧和使用者棧的切換,是如何保護現場的),下面我們都預設核心只佔高1G的地址空間,而這1G的核心空間則是所有程序共享的,也就是那句話,每個程序都有4G的虛擬地址空間,但是其中的1G是所有程序共享的核心空間,而剩餘的3G空間則是所有程序私有的使用者地址空間,程式執行在核心空間和使用者空間時的狀態又被稱為核心態和使用者態,在核心態和使用者態的執行級別是不同的,也就是許可權是不同的,什麼時候會進入核心態? 一般中斷的時候會進入核心態執行,而中斷又分為硬體中斷和軟中斷,硬體中斷,比如說你在鍵盤上輸入的時候就是硬體中斷,處理過程–保護現場–硬體處理–得到中斷向量碼–查詢中斷向量表–執行中斷向量子程式–恢復現場;軟體中斷,比較常見的就是我們呼叫系統呼叫的時候,感覺不能扯遠了,關於中斷都是學校彙編課本學習的; 關於這塊的內容建議參考**《程式設計師自我修養》**12章; 留一個問題,為什麼要有核心態和使用者態?

繼續下一個,處於核心空間和棧之間的是環境變數和命令列引數,這些是在執行main函式之前就放好的,關於環境變數,在Linux下,輸入env檢視環境變數,set 檢視所有變數,export 將普通變數變為環境變數,環境變數是會被子程序繼承的,關於驗證這一點其實也很簡單 : 你在Linux命令列上設定一個普通變數,test=’aaaa’; 檢視變數: echo $test; 此時test 是個普通變數;你用env | grep test 是找不到的,如果你用set | grep test 就可以看到設定的變數, 現在想要讓test變為環境變數: export test; test變為環境變數; 現在就是驗證子程序是否會繼承環境變量了,很簡單,命令列輸入bash就會是一個子bash了,載看看test的值還在不在,退出子bash 按 ctrl+d;下面貼出一張演示圖;命令列引數就太熟悉了,可以通過main函式的引數獲取命令列引數,就在argv裡面放著呢,不做贅述;

3.下一個,棧 不要太熟悉,可不是我們使用的容器stack啊;我們平時寫程式碼的時候,區域性變數都是在棧上存放的,而每個執行緒的預設棧的大小是1M(可以 修改),用於維護函式呼叫的上下文,(此時你就該回憶一下函式的呼叫約定,函式的棧幀等),同時圖上很明顯的可以看出棧的增長方向是從高地址向低地址;

這塊留一個問題:

object return_fun
{
    //object 是一個類,這裡返回一個類的物件;
    //方式 1
    object ret;
    return ret;

    //方式2: 優化版本
    return object();

    //問題: 方式1和方式2各呼叫了幾次拷貝建構函式?  為什麼?
}

參考《程式設計師的的自我修養》 P305

4.記憶體對映段:此處,核心將硬碟檔案的內容直接對映到記憶體,任何程式都可以通過Linux的mmap系統呼叫請求這種對映。記憶體對映是一種方便高效的檔案I/0方式,因而被用於裝載動態共享庫。使用者也可建立匿名記憶體對映,該對映沒有對應的檔案,可用於存放資料,在Linux中,若通過malloc申請一塊比較大的記憶體的話,就會使用mmap的匿名地址空間對映,這個在後面細說;

5.堆:和棧一樣使用來存放資料,由低地址向高地址增長,使用者可以申請的記憶體,在使用者釋放之前一直有效,不過對於C++來說,痛並著快樂,常常因為記憶體洩露搞得焦頭爛額,經常羨慕Java的垃圾回收機制;同時不同的作業系統對堆的維護方式不太一樣,這個下一部分細說;

6.BSS段:未初始化的資料段,但其實並不包含資料,僅維護一個開始地址和結束地址,BSS段的資料預設都是零,所以在二進位制可執行檔案的檔案頭內,其實是不存在BSS段的,所以,如果有人問你,BSS段節省的是什麼的空間,你就告訴他,節省的是檔案空間,而不是記憶體空間,因為當程式讀取BSS段的資料時,核心將會將其轉到一個全零頁面,不會發生缺頁,但會為其分配對應的實體記憶體 ; 
7.DATA段已初始化的資料,和BSS段共稱為資料段;佔檔案空間,也佔實體記憶體; 
8. 程式碼段: 也就是存放程式程式碼的地方,同時也是隻讀的,比如我們的char *str = “hello world”; 就是存放在只讀資料區,其實和程式碼段在一起,因此,在程式中我們不可以修改這個字串; 
9. 最後一部分:保留區位於虛擬地址空間的低地址處,未賦予實體地址,任何對它的引用都是非法的;比如,我們對NULL指標的操作就是非法的;

終於把上面的拖拖拉拉的說完了,當然,本篇部落格的重點是下面要說的

堆與記憶體管理

1.先來試試自己在Linux和windows最多可以分到多少堆記憶體;

測試程式碼:

#include<iostream>

using namespace std;

unsigned maximum = 0;
int main()
{
    unsigned blocksize[3] = {1024*1024,1024,1};

    int count = 0;
    for(int i = 0; i < 3; ++i)
    {
        for(count = 1; ; count++)
        {
            void* block = malloc(maximum + blocksize[i]*count);
            if(block)
            {
                maximum = maximum + blocksize[i]*count;
                free(block);
            }else{
                break;
            }
        }
    }
    printf("maxsize is %u M\n",maximum/(1024*1024));
    system("pause");
    return 0;
}

windows執行結果: 1.6 GB


Linux執行結果:  1.9 GB,2.6版本以後堆最多可以開闢2.9G的空間


虛擬地址空間中有那麼大的空間都可以被當來用作堆空間,而且2.6版本的Linux最多開闢1.9G的堆空間,2.6版本以後將記憶體對映段上調的和棧越來越近,堆最多可以開闢2.9G的空間,那麼這麼大的空間我們應該怎麼來管理呢?結合STL的空間配置器來說,為什麼要有空間配置器呢? 就是因為頻繁的向作業系統申請記憶體的效率比較低,而且如果頻繁申請小額記憶體,又會造成記憶體碎片問題,所以就直接扔給配置器一大塊空間,讓使用者空間自己去處理,而windows和linux的底層記憶體管理其實都是類似的思想,只不過更復雜罷了;
Linux程序堆管理:

linux下的程序堆管理比較複雜,提供兩種分配方式,就是兩個系統呼叫,一個是brk()系統呼叫,另外一個是mmap();
brk的作用實際上就是設定程序資料段的結束地址,即它可以擴大或者縮小資料段;我們可以將資料段擴大的那一部分作為堆空間使用,這是最常見的做法之一。而glibc還有一個函式叫sbrk(),功能和brk一致,實際上就是對brk的一層包裝,這裡就不深入探究了,沒意義;
mmap()的作用和windows下的VitualAlloc很相似,作用是向作業系統申請一段虛擬地址空間,當這塊虛擬地址可以對映到某個檔案的時候它的作用是共享記憶體,當它沒有對映到檔案的時候,那麼這塊虛擬地址空間成為匿名空間,可以被用來當做堆空間;

void *mmap

    void *start,      //申請的起始地址
    size_t length,    //長度
    int port,        
    int flags,
    int fd,
    off_t offset
);

mmap申請的虛擬地址空間大小都是頁的倍數,所以比較消耗系統性能;所以如果申請比較小的記憶體總不能總是呼叫mmap吧,所以Linux的glibc有自己的一套分配演算法;

這裡我們著重說一下mmap的是怎麼得到匿名空間的:看圖

如果mmap對映的一個vm_area_struct儲存的是一塊沒有被佔用的空間,那麼這塊空間就可以被當做堆來使用;

glibc的堆分配演算法: 對於小於64位元組的空間申請是採用類似於物件池的方法;對於大於512位元組的空間申請則是採用的最佳適配演算法(作業系統課本有講);對於大於64位元組而小於512位元組的,它會根據情況採取上述方法中的最佳折中策略;對於大於128KB的申請,它會使用mmap機制直接向作業系統申請空間;

棧和堆的比較

申請方式和效率: 
     棧是由系統自動分配釋放;堆是由程式設計師自己來申請和回收;而且在堆分配的時候還需要遍歷空閒連結串列,效率不言而喻;
申請大小的限制: 
     棧一般預設的大小是1M,超過棧剩餘空間時會報棧溢位錯誤,因此從棧上獲取的空間比較小; 
     堆如果是在新版本的Linxu可以獲取2.9G最多,在windows下一般為1.5G,最多1.9G;
儲存內容的生命週期: 
     棧一般用來做函式的呼叫棧幀,儲存區域性變數和暫存器值,所以當函式結束後,退棧清除資料; 
     堆申請的空間存放的資料,知道堆被回收,否則一直存在;
最後在遺留一個問題:malloc(0) 的返回值?
--------------------- 
作者:John__xs 
來源:CSDN 
原文:https://blog.csdn.net/bitboss/article/details/70154146 
版權宣告:本文為博主原創文章,轉載請附上博文連結!