程序員接觸的內存空間和系統接觸的物理內存空間是有所區別的。對於一般進程來講,他面對的是一個線性虛擬內存空間:地址從0到最大值。每一個進程面對的虛擬內存空間都是一樣的,都享有全部的內存地址。虛擬內存空間是線性的,但並不意味著是連續的。部分地址段的虛擬空間可以是缺失的(不是所有地址都可以用來存儲數據)。
虛擬內存可以按頁管理,每一頁大小一般為4kb。每一頁背後都有一個實際物理內存(可以是主存也可以是輔存)與之對應。在物理內存中我們不叫頁,而稱之為幀。分頁的好處就是可以在主存不夠的情況下把輔存給利用上。我們可以將暫時不用的主存頁保存到輔存中,這樣時候這塊主存頁便可以被我們覆寫,需要的時候還可以從輔存中恢復。在安裝linux系統的時候,如果內存偏小我們時常通過激活swap分區,增加虛擬內存空間。由局部性原理,速度上並不會差別太大。
並不是每一個虛擬內存頁都有實際內存幀作為其後盾。如果沒有對應的實際內存,便是缺頁的情況。有個列外:虛擬頁的數據全為0時,並不需要內存幀。只要有一個標記就可以了。
內存頁和內存幀不一定是一對一關系,剛才說了有些內存頁可能沒有內存幀,有些內存幀可能會被多個內存頁使用(多個內存頁一般分屬於多個進程,上面提到了一般進程面對的空間都是一樣的)。換句話將,一個內存幀可以被多個內存頁共享,再進一步可以被多個進程共享。典型的例子是庫函數printf函數。每一個進程都會共享庫函數printf(動態鏈接庫),所以printf實際只有一個實例,每個進程的printf函數都在相同的內存幀中。
雖說內存頁可以有內存幀作為其後盾也可以沒有,但進程要想有效的訪問內存頁,該內存頁必須要對應一個內存幀。實際情況是內存頁的需求大於內存幀的實際大小。需求太大的實際原因——每一個進程都想獨占整個物理內存。那該怎麼解決這個問題呢?聯想以下實際生活中的電梯。一個電梯最多容納10人,而一棟樓有200號人。那我們是不是需要20個電梯才夠?其實不然,或許兩三個電梯就可以了。理由是:人不會總呆在電梯裡面。再來看看我們的內存,內存頁相當與人,內存幀則相當於電梯。幀不夠了,頁出來就行。內存頁我們可以放到輔存中,比如磁盤。如果我們突然需要存儲在輔存上的內存頁時,再將輔存上的內存頁與實際內存幀對應。這一行為可以叫做換頁。
前面提到了缺頁。缺頁更嚴謹的講是進程嘗試訪問一個沒有內存幀對應的內存頁時發生的一種錯誤。這是候內核會掛起該進程,進行一些調度把內存頁和內存幀連接起來,然後再恢復進程。這被稱為換進/faulting in。值得注意的是,這種情況並不是什麼好事,會減慢程序的運行。頁的換進換出做的操作是IO操作。
內存申請,很多人可能認為只有內存不夠的時候才需要該操作。但反問一句,內存已經不夠了,你上哪兒獲取?實際上內存申請會追蹤一個進程的數據的地址,確保相同地址的內存不會存進兩個完全不同的數據。
進程申請內存有兩種方式:exec、編程方式、[fork]。
Exec 可以為進程創建虛擬地址空間,將最基本程序部分加載進去,然後執行程序。一旦程序開始執行,程序使用編程的方式獲取額外內存。GNU C庫中有兩種申請內存的方式:自動申請和動態申請。
內存映射I/O是一種動態虛擬內存申請方式。將虛擬內存的內容映射到IO設備上的常規文件。虛擬內存的任何修改都可以同步到IO上的常規文件。只有當我們訪問到了該地址段的虛擬內存,我們才需要做IO讀寫、訪問實際內存地址。所以這是一種非常有效率的讀寫方式。
進程可以用編程的方式申請內存,也可以釋放內存,但是你無法釋放以exec方式申請的內存。
虛擬內存按頁管理,而進程的虛擬地址則按段管理。合起來稱為斷頁式管理。一個段是一個連續的虛擬地址。一般有三個重要的段:
1、text segment:包含了程序的指令、字面值、還有靜態常量。由exec申請,並在整個生命周期中保持相同的大小。
2、data segment:數據段作為程序的工作空間。可以事先有exec申請加載,也可以由進程以編程的方式擴展或縮小其空間大小。但不管你怎麼擴大或縮小,本段都有一個固定的最小空間。
3、stack segment:棧段包含一個程序的棧空間。隨著程序棧的擴大而擴大,但不會隨其縮小而縮小。