操做系統思考 第三章 虛擬內存

第三章 虛擬內存

做者:Allen B. Downeyhtml

原文:Chapter 3 Virtual memorygit

譯者:飛龍github

協議:CC BY-NC-SA 4.0數組

3.1 簡明信息理論

比特是二進制的數字,也是信息的單位。一個比特有兩種可能的狀況,寫爲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之間進行轉換。

3.2 內存(Memory)和儲存器(Storage)

當進程處於運行期間,它的多數數據都放在「主存」(內存)之中,它一般是一些隨機儲存器(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。

3.3 地址空間

主存中的每一個字節都由一個「物理地址」整數所指定,物理地址的集合叫作物理「地址空間」。它的範圍一般爲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,大約比當前的物理內存大十億倍。虛擬內存比物理內存大不少,這看上去有些奇怪,可是咱們很快就就會看到它如何工做。

當一個程序讀寫內存中的值時,它使用虛擬地址。硬件在操做系統的幫助下,在訪問主存以前將物理地址翻譯虛擬地址。翻譯過程在進程層級上完成,因此即便兩個進程訪問相同的虛擬地址,它們所映射的物理地址可能不一樣。

所以,虛擬內存是操做系統隔離進程的一種重要途徑。一般,一個進程不能訪問其餘進程的數據,由於沒有任何虛擬地址能映射到其餘進程分配的物理內存。

3.4 內存段

一個運行中進程的數據組織爲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的地址最低,隨後是globalplocal的地址會更大,它是12個十六進制數字,每一個十六進制數字對應4比特,因此它是48位的地址。這代表虛擬內存的可用部分爲2 ** 48個字節。

做爲一個練習,你須要在你的電腦上運行這個程序,並將你的結果與個人結果比較。添加對malloc的第二個調用來檢查你係統上的堆區是否向上增加(地址更高)。添加一個函數來打印出局部變量的地址,檢查棧是否向下增加。

3.5 靜態局部變量

棧上的局部變量有時稱爲「自動變量」,由於它們當函數建立時自動被分配,而且當函數返回時自動被釋放。

C語言中又另外一種局部變量,叫作「靜態變量」,它分配在在static段上。它在程序啓動時初始化,而且在函數調用之間保存它的值。

例如,下面的函數跟蹤了它所調用的次數:

int times_called()
{
  static int counter = 0;
  counter++;
  return counter;
}

static關鍵字表示counter是靜態局部變量。它的初始化只發生一次,就是程序啓動的時候。

若是你將這個函數添加到aspace.c,你能夠肯定counter和全局變量一塊兒分配在static段上,而不是在棧上。

3.6 地址翻譯

虛擬地址(VA)如何翻譯成物理地址(PA)?基本的機制十分簡單,可是簡單的實現方式十分耗時,而且佔據大量空間。因此實際的實現會複雜一點。

大多數處理器提供了內存管理單元(MMU),位於CPU和主存之間。MMU在VA和PA之間執行快速的翻譯。

  1. 當程序讀寫變量時,CPU會獲得VA。

  2. MMU將VA分紅兩部分,稱爲頁碼和偏移。「頁」是一個內存塊,頁的大小取決於操做系統和硬件,一般爲1~4KiB。

  3. MMU在「頁表」裏查找頁碼,而後獲取相應的物理頁碼。以後它將物理頁碼和偏移組合獲得PA。

  4. 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中。

相關文章
相關標籤/搜索