程式設計師對記憶體的理解

2021-06-18 02:27:56 字數 3500 閱讀 1673

[收藏]

在c和c++語言開發中,指標、記憶體一直是學習的重點。因為c語言作為一種偏底層的中低階語言,提供了大量的記憶體直接操作的方法,這一方面使程式的靈活度最大化,同時也為bug埋下很多隱患。

因此,無論如何,我們都要對記憶體有乙個清晰的理解。

32位作業系統支援4gb記憶體的連續訪問,但通常把記憶體分為兩個2gb的空間,每個程序在執行時最大可以使用2gb的私有記憶體(0x00000000—0x7fffffff)。即理論上支援如下的大陣列:

char szbuffer[2*1024*1024*1024];
當然,由於在實際執行時,程式還有**段、臨時變數段、動態記憶體申請等,實際上是不可能用到上述那麼大的陣列的。

至於高階的2gb記憶體位址(0x80000000—0xffffffff),作業系統一般內部保留使用,即供作業系統核心**使用。在windows和linux平台上,一些動態鏈結庫(windows的dll,linux的so)以及ocx控制項等,由於是跨程序服務的,因此一般也在高2gb記憶體空間執行。

可以看到,每個程序都能看到自己的2gb記憶體以及系統的2gb記憶體,但是不同程序之間是無法彼此看到對方的。當然,作業系統在底層做了很多任務作,比如磁碟上的虛擬記憶體交換(請看下以標題),不同的記憶體塊動態對映等等。

虛擬記憶體的基本思想是:用廉價但緩慢的磁碟來擴充快速卻昂貴的記憶體。在一定時刻,程式實際需要使用的虛擬記憶體區段的內容就被載入物理記憶體中。當物理記憶體中的資料有一段時間未被使用,它們就可能被轉移到硬碟中,節省下來的物理記憶體空間用於載入需要使用的其他資料。

在程序執行過程中,作業系統負責具體細節,使每個程序都以為自己擁有整個位址空間的獨家訪問權。這個幻覺是通過「虛擬記憶體」實現的。所有程序共享機器的物理記憶體,當記憶體使用完時就用磁碟儲存資料。在程序執行時,資料在磁碟和記憶體之間來回移動。記憶體管理硬體負責把虛擬位址翻譯為實體地址,並讓乙個程序始終執行於系統的真正記憶體中,應用程式設計師只看到虛擬位址,並不知道自己的程序在磁碟與記憶體之間來回切換。

從潛在的可能性上說,與程序有關的所有記憶體都將被系統所使用,如果該程序可能不會馬上執行(可能它的優先順序低,也可能是它處於睡眠狀態),作業系統可以暫時取回所有分配給它的物理記憶體資源,將該程序的所有相關資訊都備份到磁碟上。

程序只能操作位於物理記憶體中的頁面。當程序引用乙個不在物理記憶體中的頁面時,mmu就會產生乙個頁錯誤。記憶體對此事做出響應,並判斷該引用是否有效。如果無效,核心向程序發出乙個「segmentation violation(段違規)」的訊號,核心從磁碟取回該頁,換入記憶體中,一旦頁面進入記憶體,程序便被解鎖,可以重新執行——程序本身並不知道它曾經因為頁面換入事件等待了一會。

對於程式設計師,我們最重要的是能理解不同程序間私有記憶體空間的含義。c和c++的編譯器把私有記憶體分為3塊:基棧、浮動棧和堆。如下圖:

(1)基棧:也叫靜態儲存區,這是編譯器在編譯期間就已經固定下來必須要使用的記憶體,如程式的**段、靜態變數、全域性變數、const常量等。

(2)浮動棧:很多書上稱為「棧」,就是程式開始執行,隨著函式、物件的一段執行,函式內部變數、物件的內部成員變數開始動態占用記憶體,浮動棧一般都有生命週期,函式結束或者物件析構,其對應的浮動棧空間的就拆除了,這部分內容總是變來變去,記憶體占用也不是固定,因此叫浮動棧。

(3)堆:c和c++語言都支援動態記憶體申請,即程式執行期可以自由申請記憶體,這部分記憶體就是在堆空間申請的。堆位於2gb的最頂端,自上向下分配,這是避免和浮動棧混到一起,不好管理。我們用到malloc和new都是從堆空間申請的記憶體,new比malloc多了物件的支援,可以自動呼叫建構函式。另外,new建立物件,其成員變數位於堆裡面。

我們來看乙個例子:

const

int n = 100;

void func(void

)

這個函式如果執行,其中n由於是全域性靜態變數,位於基棧,ch和pbuff這兩個函式內部變數,ch位於浮動棧,而pbuff指向的由malloc分配的記憶體區,則位於堆疊。

在記憶體理解上,最著名的例子就是執行緒啟動時的引數傳遞。

函式啟動乙個執行緒,很多時候需要向執行緒傳引數,但是執行緒是非同步啟動的,即很可能啟動函式已經退出了,而執行緒函式都還沒有正式開始執行,因此,絕不能用啟動函式的內部變數給執行緒傳參。道理很簡單,函式的內部變數在浮動棧,但函式退出時,浮動棧自動拆除,記憶體空間已經被釋放了。當執行緒啟動時,按照給的引數指標去查詢變數,實際上是在讀一塊無效的記憶體區域,程式會因此而崩潰。

那怎麼辦呢?我們應該直接用malloc函式給需要傳遞的引數分配一塊記憶體區域,將指標傳入執行緒,執行緒收到後使用,最後執行緒退出時,free釋放。

我們來看例子:

//

這個結構體就是參數列

typedef struct _clisten_listenaccepttask_param_

sclistenaccepttaskparam;

//習慣性寫法,設定結構體後,立即宣告結構體的尺寸,為後續malloc提供方便

const ulong sclistenaccepttaskparamsize = sizeof(sclistenaccepttaskparam);

//這裡接收到連線請求,申請引數區域,將關鍵資訊帶入引數區域,幫助後續執行緒工作。

bool clisten::listentaskcallback(void* pcallparam,int&nstatus) //

這是執行緒函式,負責處理上文accept到的socket

bool clisten::listenaccepttask(void* pcallparam,int&nstatus)

無規則的濫用記憶體和指標會導致大量的bug,程式設計師應該對記憶體的使用保持高度的敏感性和警惕性,謹慎地使用記憶體資源。

使用記憶體時最容易出現的bug是:

(1)壞指標值錯誤:在指標賦值之前就用它來引用記憶體,或者向庫函式傳送乙個壞指標,第三種可能導致壞指標的原因是對指標進行釋放之後再訪問它的內容。可以修改free語句,在指標釋放之後再將它置為空值。

free(p); p = null;
這樣,如果在指標釋放之後繼續使用該指標,至少程式能在終止之前進行資訊轉儲。

(2)改寫(overwrite)錯誤:越過陣列邊界寫入資料,在動態分配的記憶體兩端之外寫入資料,或改寫一些堆管理資料結構(在動態分配記憶體之前的區域寫入資料就很容易發生這種情況)

p = malloc(256); p[-1] = 0; p[256] = 0;
(3)指標釋放引起的錯誤:釋放同乙個記憶體塊兩次,或釋放一塊未曾使用malloc分配的記憶體,或釋放仍在使用中的記憶體,或釋放乙個無效的指標。乙個極為常見的與釋放記憶體有關的錯誤就是在 for(p=start;p=p->next) 這樣的迴圈中迭代乙個鍊錶,並在迴圈體內使用 free(p) 語句。這樣,在下一次迴圈迭代時,程式就會對已經釋放的指標進行解除引用操作,從而導致不可預料的結果。

我們可以這樣迭代:

struct node *p, *tart, *temp;

for(p = start; p ; p =temp)

總結:這些知識都是本人最近看書總結出來的,可能有很多是個人主觀,歡迎拍磚…

對記憶體位元組的理解

為了加強自己的記憶,我決定用文字記錄下來。這是源 主要是輸出環境變數,具體可以參考createenvironmentblock函式 1 include 2 include 3 include 4 include 5 pragma comment lib,userenv.lib 6 pragma wa...

對記憶體的深入理解

隨機訪問儲存器 ram 能夠與cpu直接的進行資料的交換 可以隨時的進行讀寫,速度快 斷電後不儲存資料 唯讀儲存器 rom 只能讀取資料不能寫入資料 非易失性 時序控制邏輯 產生儲存器操作所需的各種時序訊號 位址暫存器mar 存放訪存位址,位址解碼後找到所選的儲存單元 資料暫存器mdr 暫存要從儲存...

程式設計師的閱讀理解

美國的貝爾實驗室設計了最初的c語言 刻在unix作業系統距今已有三四十年 你在螢幕前凝視資料的繾綣 我卻在旁輕輕敲打鍵盤把你的夢想展現 迴圈 遞迴 貪心 動規 是誰的從前 喜歡在匈牙利演算法中你我牽手的畫面 經過msra門前我以大牛之名許願 思念像斐波那契數列般漫延 當軟工淪落在設計的文件間 演算法...