程式地址空間的理解

2020-09-22 12:00:26

程式:只是一段程式碼,儲存在檔案中。

編譯器在編譯程式生成可執行檔案時,會對每一條指令和資料,進行地址排號。

程式執行時,就會將指令和資料放到指定的記憶體當中去。而程式只有在執行的時候才會佔據記憶體,因此程式地址空間又被叫做程序地址空間。
在這裡插入圖片描述

記憶體空間是這樣的。
若執行中的程式直接存取實體地址,會怎麼樣呢?

  1. 可能導致程式無法執行起來。 程式在編譯時,會給變數資料進行地址排號,但若是某個地址被佔用了,就會使程式無法執行起來。(編譯器無法動態的獲取哪一塊記憶體是否被使用)
  2. 野指標問題。 若程序直接存取實體地址,野指標可能更改其它程序的資料。
  3. 記憶體使用率低。 程式執行需要一塊連續的地址空間,會一定程度上造成空間的浪費。

所以OS中設定了虛擬記憶體,通過虛擬地址空間對映到實體記憶體上。而使用C語言/C++時,變數或函數的地址,都是虛擬空間地址,實體記憶體地址使用者一概看不到,由OS統一管理。而OS負責將虛擬地址對映到對應的實體地址

每執行一段程式,就會開闢連續的地址空間,若是每個程式佔據的空間比較大,很多程式共同執行,就會導致有的程式在記憶體中無法執行。而連續開闢的記憶體地址空間的空間使用率是很低的。

程序使用了虛擬記憶體之後,每個程序都擁有自己的虛擬地址空間,都會有一塊連續的空間使用。

看一下這段程式碼:

   #include <stdio.h>
   #include <stdlib.h>
   #include <unistd.h>
  
   int global_val = 200;
   
   int main()
   {
       pid_t pid = fork();//建立子程序
      if(pid < 0)
      {
          printf("fork error\n");
          return 0;
      }
      else if(pid == 0)
      {
          printf("child:%d  %p\n",global_val,&global_val);
      }
      else
      {
          printf("parent:%d   %p\n",global_val,&global_val);
      }
      return 0;                                                                                                                                                                             
   }

其輸出為:
在這裡插入圖片描述
發現子程序中和父程序使用的是同樣的變數和地址。

對程式碼進行一點小更改:

   #include <stdio.h>
   #include <stdlib.h>
   #include <unistd.h>
  
   int global_val = 200;
   
   int main()
   {
       pid_t pid = fork();//建立子程序
      if(pid < 0)
      {
          printf("fork error\n");
          return 0;
      }
      else if(pid == 0)
      {
          global_val = 100;
          printf("child:%d  %p\n",global_val,&global_val);
      }
      else
      {
          sleep(3);
          printf("parent:%d   %p\n",global_val,&global_val);
      }
      return 0;                                                                                                                                                                             
   }

其輸出為:
在這裡插入圖片描述
可以看到子程序的變數改變了,而父程序的變數是沒有改變的。

為什麼子程序變數改變了,而父程序的變數沒有改變?
子程序是父程序的一份拷貝,子程序拷貝了父程序所有的資訊。在子程序中資料未發生改變的時候,子程序使用父程序的所有資訊。

在第一份程式碼中,子程序中變數沒有改變,父程序中變數沒有改變。所以第一份程式碼中,地址相等,變數也相等。

第二份程式碼中,子程序中變數發生了改變,父程序中變數沒有發生改變。相同的虛擬地址對映到了不同的實體地址。所以第二份程式碼,地址相同,變數不同。

這裡的相同是指:子程序拷貝了父程序所有的資訊,程序地址空間、PCB…

在這裡插入圖片描述
子程序資料發生更改,進行了拷貝了。

第二份程式碼中,這裡涉及到了寫時拷貝技術:Linux中fork()使用寫時拷貝實現。寫時拷貝是一種推遲或者免除拷貝的技術。OS並不複製整個程序地址空間,而是子程序父程序共用一個地址,當有資料寫入,發生改變時,資料才會被複制,使每個程序都有了自己的拷貝。資源的複製只有在寫入的時候才進行。 而在此之前,子程序只是可讀共用的,這樣就保證了父子程序的程式碼共用,資料獨立。

寫時拷貝技術帶來的好處:

  1. 提高子程序建立效率。
  2. 節省資源。

那麼為什麼OS要使用虛擬地址空間?或者說虛擬地址空間帶了什麼好處?

  1. 提高實體記憶體的使用率。
  2. 保證程序之間的獨立性

虛擬地址是如何對映到實體地址的?

作業系統中記憶體管理方式:

  1. 分段式:段號+段內偏移在這裡插入圖片描述

段表:作業系統記錄記憶體分了多少塊。
通過段號尋找對應的實體記憶體起始地址,再加上段內偏移量,就找到了實體地址。

  1. 分頁式:頁號+頁內偏移,這裡畫的比較簡單在這裡插入圖片描述
    在這裡有所不同的時候,我們需要知道一個頁面的大小。一般一個頁面的大小為4K。
    則在32位元OS中,記憶體為4G,則佔有4* 1024* 1024* 1024/4* 1024個頁號,即頁表項。
    共計有2^20個頁表項/頁號。將記憶體分為很多個細小的塊。

通過找到對應的頁號,其實體地址和頁內偏移就可找到變數的實體地址。

  1. 段頁式:記憶體通過分段式進行管理,而每個段內使用分頁式。
    首先取得段號,在段表中進行查詢;在段表中,存放著對應段號的頁表起始地址,再通過段內頁表起始地址找到頁表。

當前計算機使用的段頁式管理

虛擬頁會快取在實體記憶體中。如圖:
在這裡插入圖片描述
虛擬記憶體可快取到頁表中:頁命中,VP2就會快取在記憶體中。
快取不命中:缺頁 ,VP3不會命中,發生缺頁中斷。那麼OS就會從磁碟複製VP3到記憶體中PP3,再更新PTE3,隨後返回。
VP3:虛擬記憶體3.
PP3:實體記憶體3
PTE3:頁表條目3。0:發生中斷,1:可以快取。
在這裡插入圖片描述
經過缺頁中斷之後:缺頁處理程式會選擇一個作為犧牲頁,並從磁碟上VP3的副本取代它,

MMU利用頁表來實現虛擬地址空間到實體地址空空間的對映:
在這裡插入圖片描述

那麼該選擇犧牲頁呢?
採用記憶體置換演演算法

  1. OPT:最佳置換演演算法,所被置換出的頁面是以後永遠不會再使用的或者是最長時間內不會再使用。這個演演算法只是理論上的演演算法。
  2. FIFO:先進先出演演算法。會導致缺頁率升高。
  3. LRU:最久未使用演演算法,將最久未使用的頁面置換出來。 (一般使用這個演演算法)
  4. LFU:最不常用演演算法,一段時間內使用的次數最少,在將來使用的可能性也很低。