做者:Allen B. Downeyhtml
原文:Chapter 3 Virtual memorygit
譯者:飛龍github
協議:CC BY-NC-SA 4.0數組
比特是二進制的數字,也是信息的單位。一個比特有兩種可能的狀況,寫爲0或者1。若是是兩個比特,那就有四種可能的組合,00、0一、10和11。一般,若是你有b
個比特,你就能夠表示2 ** b
個值之一。一個字節是8個比特,因此它能夠儲存256個值之一。函數
從其它方面來說,假設你想要儲存字母表中的字母。字母共有26個,因此你須要多少個比特呢?使用4個比特你能夠表示16個值之一,這是不夠的。使用5個比特你能夠表示32個值,這對於全部字母是夠用的,同時還有一點點浪費。佈局
一般,若是你想要表示N
個值之一,你就須要求出最小的b
使2 ** b >= N
。在兩邊計算以2爲底的對數,就會獲得b >= log(2, N)
。編碼
假設我投擲一枚硬幣而且告訴你結果,我就向你提供了1比特的信息。若是我投擲六個面的篩子並告訴你結果,我就向你提供了log(2, 6)
比特的信息。而且一般,若是結果的機率是1/n
,結果應該包含log(2, N)
比特的信息。spa
一樣,若是結果的機率爲p
,那麼信息的內容爲-log(2, p)
。這個數量叫作「自信息」(self-information)。它度量告終果有多麼使人意外,因此也叫做「驚異度」。若是你的賽馬只有十六分之一的概率獲勝,而且它獲勝了,那麼你就獲得了4比特的信息(以及獎金)。可是若是它的獲勝概率爲75%,這條新聞只含有0.42個比特。操作系統
能夠由直以爲出,非預期的新聞會帶有大量信息;與之相反,若是你對一件事情頗有自信,對它的驗證只會獲得少許的信息。翻譯
對於書中的一些話題,咱們只須要熟練於在比特數量b
和它們所編碼的值的數量N = 2 ** b
之間進行轉換。
當進程處於運行期間,它的多數數據都放在「主存」(內存)之中,它一般是一些隨機儲存器(RAM)。在當前的大多數電腦上,主存很是易失,也就是說,當電腦關閉時,主存的內容就沒了。一個典型的臺式電腦擁有2~8GiB的內存。GiB表明「gibibyte」,至關於2 ** 30
個字節。
若是進程會讀寫文件,這些文件一般放在機械硬盤(HDD)或固態硬盤(SSD)裏面。這些儲存器都是非易失的,因此他們可用於長時間儲存。當前,一個典型的臺式電腦擁有500GB到2TB的HDD。GB表明「gigabyte」,至關於10 ** 9
個字節。TB表明「terabyte」,至關於10 ** 12
個字節。
你可能會注意到我使用二進制單位GiB來描述主存大小,並使用十進制單位GB和TB來描述HDD的大小。因爲歷史和技術因素,內存以二進制單位度量,而且硬盤以十進制單位度量。本書中我會當心區分二進制和十進制單位,可是你應該注意到「gigabyte」以及GB縮寫一般在使用上很是模糊。
非正式的用法中,「內存」有時會用於HDD和SSD(特別是移動設備),以及RAM。然而,這些設備的屬性截然不同,因此咱們須要區分它們。我會使用「儲存器」來指代HDD和SSD。
主存中的每一個字節都由一個「物理地址」整數所指定,物理地址的集合叫作物理「地址空間」。它的範圍一般爲0到N-1
,其中N
是主存的大小。在帶有1GiB主存的的系統上,最高的有效地址是2 ** 30 - 1
,十進制表示爲1,073,741,823,16進製表示爲0x03ff ffff(前綴0x
表示十六進制)。
然而,許多操做系統提供「虛擬內存」,也就是說程序永遠不須要處理物理地址,也不須要知道有多少物理內存是有效的。
做爲代替,程序處理虛擬地址,它被編碼爲從0到M-1
,其中M
是有效虛擬地址的大小。虛擬地址空間的大小取決於所處的操做系統和硬件。
你必定聽過人們談論32位和64位系統。這些術語代表了寄存器的尺寸,也一般是虛擬地址的大小。在32位系統上,虛擬地址是32位的,也就是說虛擬地址空間爲從0到0xffff ffff。這一地址空間的大小是2 ** 32
個字節,或者4GiB。
在64位系統上,虛擬地址空間大小爲2 ** 64
個字節,或者4 * 1024 ** 6
個字節。這是16個EiB,大約比當前的物理內存大十億倍。虛擬內存比物理內存大不少,這看上去有些奇怪,可是咱們很快就就會看到它如何工做。
當一個程序讀寫內存中的值時,它使用虛擬地址。硬件在操做系統的幫助下,在訪問主存以前將物理地址翻譯虛擬地址。翻譯過程在進程層級上完成,因此即便兩個進程訪問相同的虛擬地址,它們所映射的物理地址可能不一樣。
所以,虛擬內存是操做系統隔離進程的一種重要途徑。一般,一個進程不能訪問其餘進程的數據,由於沒有任何虛擬地址能映射到其餘進程分配的物理內存。
一個運行中進程的數據組織爲4個段:
text
段包含程序文本,即程序所組成的機器語言指令、
static
段包含由編譯器所分配的變量,包括全局變量,和使用static
聲明的局部變量。
stack
段包含運行時棧,它由棧幀組成。每一個棧幀包含函數參數、本地變量以及其它。
heap
段包含運行時分配的內存塊,一般經過調用C標準庫函數malloc
來分配。
這些段的組織方式部分取決於編譯器,部分取決於操做系統。不一樣的操做系統中細節可能不一樣,可是下面這些是共同的:
text
段靠近內存「底部」,即接近0的地址。
static
段一般恰好在text
段上面。
stack
段靠近內存頂部,即接近虛擬地址空間的最大地址。在擴張過程當中,它向低地址的方向增加。
heap
一般在static
段的上面。在擴張過程當中,它向高地址的方向增加。
爲了搞清楚這些段在你操做系統上的佈局,能夠嘗試運行這個程序,它就是這本書的倉庫中的aspace.c
:
#include <stdio.h> #include <stdlib.h> int global; int main () { int local = 5; void *p = malloc(128); printf ("Address of main is %p\n", main); printf ("Address of global is %p\n", &global); printf ("Address of local is %p\n", &local); printf ("Address of p is %p\n", p); }
main
是函數的名稱,當它用做變量時,它指向main
中第一條機器語言指令的地址,咱們認爲它在text
段內。
global
是一個全局變量,因此咱們認爲它在static
段內。local
是一個局部變量,因此咱們認爲它在棧上。
p
持有malloc
所返回的地址,它指向堆區所分配的空間。malloc
表明「內存分配」(memory allocate)。
格式化佔位符%p
告訴printf
把每一個地址格式化爲「指針」,它是地址的另外一個名字。
當我運行這個程序時,輸出就像下面這樣(我添加了空格使它更加易讀):
Address of main is 0x 40057c Address of global is 0x 60104c Address of local is 0x7fffd26139c4 Address of p is 0x 1c3b010
正如預期的那樣,main
的地址最低,隨後是global
和p
。local
的地址會更大,它是12個十六進制數字,每一個十六進制數字對應4比特,因此它是48位的地址。這代表虛擬內存的可用部分爲2 ** 48
個字節。
做爲一個練習,你須要在你的電腦上運行這個程序,並將你的結果與個人結果比較。添加對malloc
的第二個調用來檢查你係統上的堆區是否向上增加(地址更高)。添加一個函數來打印出局部變量的地址,檢查棧是否向下增加。
棧上的局部變量有時稱爲「自動變量」,由於它們當函數建立時自動被分配,而且當函數返回時自動被釋放。
C語言中又另外一種局部變量,叫作「靜態變量」,它分配在在static
段上。它在程序啓動時初始化,而且在函數調用之間保存它的值。
例如,下面的函數跟蹤了它所調用的次數:
int times_called() { static int counter = 0; counter++; return counter; }
static
關鍵字表示counter
是靜態局部變量。它的初始化只發生一次,就是程序啓動的時候。
若是你將這個函數添加到aspace.c
,你能夠肯定counter
和全局變量一塊兒分配在static
段上,而不是在棧上。
虛擬地址(VA)如何翻譯成物理地址(PA)?基本的機制十分簡單,可是簡單的實現方式十分耗時,而且佔據大量空間。因此實際的實現會複雜一點。
大多數處理器提供了內存管理單元(MMU),位於CPU和主存之間。MMU在VA和PA之間執行快速的翻譯。
當程序讀寫變量時,CPU會獲得VA。
MMU將VA分紅兩部分,稱爲頁碼和偏移。「頁」是一個內存塊,頁的大小取決於操做系統和硬件,一般爲1~4KiB。
MMU在「頁表」裏查找頁碼,而後獲取相應的物理頁碼。以後它將物理頁碼和偏移組合獲得PA。
PA傳遞給主存,用於讀寫指定地址。
做爲一個例子,假設VA爲32位,物理內存爲1GiB,劃分爲1KiB的頁面。
因爲1GiB爲2 ** 30
個字節,物理頁的數量爲2 ** 20
個,它們也稱爲「幀」。
虛擬地址空間的大小爲2 ** 32
字節,這個例子中,頁的大小爲2 ** 10
字節,因此共有2 ** 22
個虛擬頁。
偏移的大小取決於頁的大小。這個例子中頁的大小爲2 ** 10
字節,因此須要10位來指定頁中的一個字節。
若是VA是32位,而偏移是10位,剩餘的22位構成了虛擬頁碼。
因爲共有2 ** 20
個物理頁,每一個物理頁碼是20位。加上10位的偏移,PA的結果爲30位。
到目前爲止,看上去是是可行的。可是讓咱們考慮一下頁表應該佔多大。頁表最簡單的實現是一個數組,每一個虛擬頁面是一個條目。每一個條目都包含一個物理頁碼,在例子中它是20位,加上每幀的一些額外的數據,因此咱們認爲每一個條目佔用3~4個字節。因爲共有2 ** 22
個虛擬頁,頁面共須要2 ** 24
個字節,或16MiB。
因爲咱們須要爲每一個進程建立一個頁表,一個運行256個進程的系統就須要2 ** 32
個字節,或者4GiB,這還只是頁面的空間!這些就佔用了所有32位虛擬地址。而在48或64位的虛擬地址上,這個數量更加荒謬。
幸運的是,並不須要這麼大的空間,由於大多數進程不使用虛擬地址空間的每一個小片斷。並且,若是一個進程不使用某個虛擬頁面,咱們也不須要在頁表中爲其分配條目。
也就是說,頁表是「稀疏」的,這暗示了最簡單的實現,即頁表條目的數組是個糟糕的想法。幸運的是,稀疏數組有一些不錯的實現方式。
一種選擇是多級頁表,它被多數操做系統例如Linux所採用。另外一種選擇是關聯表,其中每一個條目包含虛擬頁碼和物理頁碼。在軟件上搜索關聯表會很是慢,可是硬件上咱們能夠並行搜索整個表,因此關聯數組常常用於在MMU中表示頁表。
你能夠在頁表的維基百科頁面閱讀更多關於這些實現的信息。你也可能會找到有趣的細節。可是基本的想法就是頁表應作成稀疏的,因此咱們須要爲稀疏數組選擇一個好的實現方式。
我以前提到了操做系統能夠中斷一個運行中的進程,保存它的狀態,以後運行其它進程。這個機制叫作「上下文切換」。因爲每一個進程都有本身的頁表,操做系統須要和MMU配合來保證每一個進程拿到了正確的頁表。在舊機器上,MMU中的頁表信息在每次上下文切換時會被替換掉,開銷很是大。在新的系統中,MMU的每一個頁表條目包含進程ID,因此多個進程的頁表能夠同時儲存在MMU中。