Linux 操作系統原理 — 虛擬內存

目錄

爲什麼需要虛擬內存?

在現代操作系統中,多任務已是標配。多任務並行,大大提升了 CPU 利用率,但卻引出了多個進程對內存操作的衝突問題,虛擬內存概念的提出就是爲了解決這個問題。

在這裏插入圖片描述
操作系統有一塊物理內存(中間的部分),有兩個進程(實際會更多)P1 和 P2,操作系統偷偷地分別告訴 P1 和 P2,我的整個內存都是你的,隨便用,管夠。可事實上呢,操作系統只是給它們畫了個大餅,這些內存說是都給了 P1 和 P2,實際上只給了它們一個序號而已。只有當 P1 和 P2 真正開始使用這些內存時,系統纔開始使用輾轉挪移,拼湊出各個塊給進程用,P2 以爲自己在用 A 內存,實際上已經被系統悄悄重定向到真正的 B 去了,甚至,當 P1 和 P2 共用了 C 內存,他們也不知道。

操作系統的這種欺騙進程的手段,就是虛擬內存。對 P1 和 P2 等進程來說,它們都以爲自己佔用了整個內存,而自己使用的物理內存的哪段地址,它們並不知道也無需關心。

虛擬內存

虛擬內存是計算機系統內存管理的一種技術。它使得應用程序認爲它擁有連續的可用的內存(一個連續完整的地址空間)。

而實際上,虛擬內存通常是被分隔成多個物理內存碎片,還有部分暫時存儲在外部磁盤存儲器上,在需要時進行數據交換,加載到物理內存中來。目前,大多數操作系統都使用了虛擬內存,如 Windows 系統的虛擬內存、Linux 系統的交換空間等等。

虛擬內存地址和用戶進程緊密相關,一般來說不同進程裏的同一個虛擬地址指向的物理地址是不一樣的,所以離開進程談虛擬內存沒有任何意義。每個進程所能使用的虛擬地址大小和 CPU 位數有關。在 32 位的系統上,虛擬地址空間大小是 232=4G,在 64 位系統上,虛擬地址空間大小是 264=16G,而實際的物理內存可能遠遠小於虛擬內存的大小。

每個用戶進程維護了一個單獨的頁表(Page Table),虛擬內存和物理內存就是通過這個頁表實現地址空間的映射的。

在這裏插入圖片描述

當進程執行一個程序時,需要先從內存中讀取該進程的指令,然後執行,獲取指令時用到的就是虛擬地址。這個虛擬地址是程序鏈接時確定的(內核加載並初始化進程時會調整動態庫的地址範圍)。

爲了獲取到實際的數據,CPU 需要將虛擬地址轉換成物理地址,CPU 轉換地址時需要用到進程的頁表(Page Table),而頁表(Page Table)裏面的數據由操作系統維護。其中頁表(Page Table)可以簡單的理解爲單個內存映射(Memory Mapping)的鏈表(當然實際結構很複雜)。

裏面的每個內存映射(Memory Mapping)都將一塊虛擬地址映射到一個特定的地址空間(物理內存或者磁盤存儲空間)。每個進程擁有自己的頁表(Page Table),和其他進程的頁表(Page Table)沒有關係。

Linux 進程的虛擬內存

Linux 的虛擬存儲器涉及三個概念: 虛擬存儲空間,磁盤空間,內存空間。
在這裏插入圖片描述
可以認爲虛擬空間都被映射到了磁盤空間中,(事實上也是按需要映射到磁盤空間上,通過 mmap),並且由頁表記錄映射位置,當訪問到某個地址的時候,通過頁表中的有效位,可以得知此數據是否在內存中,如果不是,則通過缺頁異常,將磁盤對應的數據拷貝到內存中,如果沒有空閒內存,則選擇犧牲頁面,替換其他頁面。

mmap 是用來建立從虛擬空間到磁盤空間的映射的,可以將一個虛擬空間地址映射到一個磁盤文件上,當不設置這個地址時,則由系統自動設置,函數返回對應的內存地址(虛擬地址),當訪問這個地址的時候,就需要把磁盤上的內容拷貝到內存了,然後就可以讀或者寫,最後通過 man_map 可以將內存上的數據換回到磁盤,也就是解除虛擬空間和內存空間的映射,這也是一種讀寫磁盤文件的方法,也是一種進程共享數據的方法,即共享內存。

  1. 每個進程都有自己獨立的 4G 內存空間,各個進程的內存空間具有類似的結構。
  2. 一個新進程建立的時候,將會建立起自己的內存空間,此進程的數據,代碼等從磁盤拷貝到自己的進程空間,哪些數據在哪裏,都由進程控制表中的 task_struct 記錄,task_struct 中記錄中一條鏈表,記錄中內存空間的分配情況,哪些地址有數據,哪些地址無數據,哪些可讀,哪些可寫,都可以通過這個鏈表記錄。
  3. 每個進程已經分配的內存空間,都與對應的磁盤空間映射。

在這裏插入圖片描述

  1. 每個進程的 4G 內存空間只是虛擬內存空間,每次訪問內存空間的某個地址,都需要把地址翻譯爲實際物理內存地址。
  2. 所有進程共享同一物理內存,每個進程只把自己目前需要的虛擬內存空間映射並存儲到物理內存上。
  3. 進程要知道哪些內存地址上的數據在物理內存上,哪些不在,還有在物理內存上的哪裏,需要用頁表來記錄。
  4. 頁表的每一個表項分兩部分,第一部分記錄此頁是否在物理內存上,第二部分記錄物理內存頁的地址(如果在的話)。
  5. 當進程訪問某個虛擬地址,去看頁表,如果發現對應的數據不在物理內存中,則缺頁異常。
  6. 缺頁異常的處理過程,就是把進程需要的數據從磁盤上拷貝到物理內存中,如果內存已經滿了,沒有空地方了,那就找一個頁覆蓋,當然如果被覆蓋的頁曾經被修改過,需要將此頁寫回磁盤。

在這裏插入圖片描述

用戶進程申請並訪問物理內存(或磁盤存儲空間)的過程總結如下:

  1. 用戶進程向操作系統發出內存申請請求。
  2. 系統會檢查進程的虛擬地址空間是否被用完,如果有剩餘,給進程分配虛擬地址。
  3. 系統爲這塊虛擬地址創建內存映射(Memory Mapping),並將它放進該進程的頁表(Page Table)。
  4. 系統返回虛擬地址給用戶進程,用戶進程開始訪問該虛擬地址。
  5. CPU 根據虛擬地址在此進程的頁表(Page Table)中找到了相應的內存映射(Memory Mapping),但是這個內存映射(Memory Mapping)沒有和物理內存關聯,於是產生缺頁中斷。
  6. 操作系統收到缺頁中斷後,分配真正的物理內存並將它關聯到頁表相應的內存映射(Memory Mapping)。中斷處理完成後,CPU 就可以訪問內存了
  7. 當然缺頁中斷不是每次都會發生,只有系統覺得有必要延遲分配內存的時候才用的着,也即很多時候在上面的第 3 步系統會分配真正的物理內存並和內存映射(Memory Mapping)進行關聯。

另外,值得注意的是,在每個進程創建加載時,內核只是爲進程 “創建” 了虛擬內存的佈局,具體就是初始化進程控制表中內存相關的鏈表,實際上並不立即就把虛擬內存對應位置的程序數據和代碼(e.g. .text、.data 段)拷貝到物理內存中,只是建立好虛擬內存和磁盤文件之間的映射就好(叫做存儲器映射),等到運行到對應的程序時,纔會通過缺頁異常,來拷貝數據。還有進程運行過程中,要動態分配內存,比如:malloc 時,也只是分配了虛擬內存,即爲這塊虛擬內存對應的頁表項做相應設置,當進程真正訪問到此數據時,才引發缺頁異常。

在用戶進程和物理內存(磁盤存儲器)之間引入虛擬內存主要有以下的優點:

  • 地址空間:提供更大的地址空間,並且地址空間是連續的,使得程序編寫、鏈接更加簡單。
  • 進程隔離:不同進程的虛擬地址之間沒有關係,所以一個進程的操作不會對其他進程造成影響。
  • 數據保護:每塊虛擬內存都有相應的讀寫屬性,這樣就能保護程序的代碼段不被修改,數據塊不能被執行等,增加了系統的安全性。
  • 內存映射:有了虛擬內存之後,可以直接映射磁盤上的文件(可執行文件或動態庫)到虛擬地址空間。這樣可以做到物理內存延時分配,只有在需要讀相應的文件的時候,纔將它真正的從磁盤上加載到內存中來,而在內存吃緊的時候又可以將這部分內存清空掉,提高物理內存利用效率,並且所有這些對應用程序都是透明的。
  • 共享內存:比如動態庫只需要在內存中存儲一份,然後將它映射到不同進程的虛擬地址空間中,讓進程覺得自己獨佔了這個文件。進程間的內存共享也可以通過映射同一塊物理內存到進程的不同虛擬地址空間來實現共享。
  • 物理內存管理:物理地址空間全部由操作系統管理,進程無法直接分配和回收,從而系統可以更好的利用內存,平衡進程間對內存的需求。
  • 既然每個進程的內存空間都是一致而且固定的,所以鏈接器在鏈接可執行文件時,可以設定內存地址,而不用去管這些數據最終實際的內存地址,這是有獨立內存空間的好處。
  • 當不同的進程使用同樣的代碼時,比如庫文件中的代碼,物理內存中可以只存儲一份這樣的代碼,不同的進程只需要把自己的虛擬內存映射過去就可以了,節省內存。
  • 在程序需要分配連續的內存空間的時候,只需要在虛擬內存空間分配連續空間,而不需要實際物理內存的連續空間,可以利用碎片。

SWAP 交換內存

通常,Linux 的內存已滿並且內核沒有可寫空間時,系統就會崩潰。但如果系統擁有交換分區,那麼 Linux 內核和程序就會使用它,但是速度會慢很多。因此,擁有 SWAP 交換空間更安全。

交換分區有一個缺點:它比 RAM 慢很多,因此,添加交換空間不會使你的計算機運行速度更快,它只會幫助克服一些內存不足帶來的限制。

在這裏插入圖片描述
虛擬內存的 SWAP 特性並不總是有益,放任進程不停地將數據在內存與磁盤之間大量交換會極大地佔用 CPU,降低系統運行效率,所以有時候我們並不希望使用 SWAP。可以修改 vm.swappiness=0 來設置內存儘量少使用 SWAP,或者乾脆使用 swapoff 命令禁用掉 SWAP。

虛擬內存的分配

虛擬內存的分配,包括用戶空間虛擬內存和內核空間虛擬內存。

注意,分配的虛擬內存還沒有映射到物理內存,只有當訪問申請的虛擬內存時,纔會發生缺頁異常,再通過上面介紹的夥伴系統和 slab 分配器申請物理內存。

用戶空間內存分配(malloc)

malloc 用於申請用戶空間的虛擬內存,當申請小於 128KB 小內存的時,malloc 使用 sbrk 或 brk 分配內存;當申請大於 128KB 的內存時,使用 mmap 函數申請內存;

存在的問題:由於 brk/sbrk/mmap 屬於系統調用,如果每次申請內存都要產生系統調用開銷,CPU 在用戶態和內核態之間頻繁切換,非常影響性能。而且,堆是從低地址往高地址增長,如果低地址的內存沒有被釋放,高地址的內存就不能被回收,容易產生內存碎片。

解決:因此,malloc 採用的是內存池的實現方式,先申請一大塊內存,然後將內存分成不同大小的內存塊,然後用戶申請內存時,直接從內存池中選擇一塊相近的內存塊分配出去。

在這裏插入圖片描述

內核空間內存分配

先來回顧一下內核地址空間。

在這裏插入圖片描述
kmalloc 和 vmalloc 分別用於分配不同映射區的虛擬內存。
在這裏插入圖片描述

kmalloc

kmalloc() 分配的虛擬地址範圍在內核空間的直接內存映射區。

按字節爲單位虛擬內存,一般用於分配小塊內存,釋放內存對應於 kfree ,可以分配連續的物理內存。函數原型在 <linux/kmalloc.h> 中聲明,一般情況下在驅動程序中都是調用 kmalloc() 來給數據結構分配內存。

kmalloc 是基於 Slab 分配器的,同樣可以用 cat /proc/slabinfo 命令,查看 kmalloc 相關 slab 對象信息,下面的 kmalloc-8、kmalloc-16 等等就是基於 Slab 分配的 kmalloc 高速緩存。

在這裏插入圖片描述

vmalloc

vmalloc 分配的虛擬地址區間,位於 vmalloc_start 與 vmalloc_end 之間的動態內存映射區。

一般用分配大塊內存,釋放內存對應於 vfree,分配的虛擬內存地址連續,物理地址上不一定連續。函數原型在 <linux/vmalloc.h> 中聲明。一般用在爲活動的交換區分配數據結構,爲某些 I/O 驅動程序分配緩衝區,或爲內核模塊分配空間。

CPU 是如何訪問內存的?

在這裏插入圖片描述

在這裏插入圖片描述

從圖中可以清晰地看出,CPU、MMU、DDR 這三部分在硬件上是如何分佈的。首先 CPU 在訪問內存的時候都需要通過 MMU 把虛擬地址轉化爲物理地址,然後通過總線訪問內存。MMU 開啓後 CPU 看到的所有地址都是虛擬地址,CPU 把這個虛擬地址發給 MMU 後,MMU 會通過頁表在頁表裏查出這個虛擬地址對應的物理地址是什麼,從而去訪問外面的 DDR(內存條)。

所以搞懂了 MMU 如何把虛擬地址轉化爲物理地址也就明白了 CPU 是如何通過 MMU 來訪問內存的。

MMU 是通過頁表把虛擬地址轉換成物理地址,頁表是一種特殊的數據結構,放在系統空間的頁表區存放邏輯頁與物理頁幀的對應關係,每一個進程都有一個自己的頁表。

CPU 訪問的虛擬地址可以分爲:p(頁號),用來作爲頁表的索引;d(頁偏移),該頁內的地址偏移。現在我們假設每一頁的大小是 4KB,而且頁表只有一級,那麼頁表長成下面這個樣子(頁表的每一行是 32 個 bit,前 20 bit 表示頁號 p,後面 12 bit 表示頁偏移 d):

在這裏插入圖片描述
CPU,虛擬地址,頁表和物理地址的關係如下圖:
在這裏插入圖片描述

頁表包含每頁所在物理內存的基地址,這些基地址與頁偏移的組合形成物理地址,就可送交物理單元。

上面我們發現,如果採用一級頁表的話,每個進程都需要 1 個 4MB 的頁表(假如虛擬地址空間爲 32 位,即 4GB、每個頁面映射 4KB 以及每條頁表項佔 4B,則進程需要 1M 個頁表項,4GB/4KB = 1M),即頁表(每個進程都有一個頁表)佔用 4MB(1M * 4B = 4MB)的內存空間)。

然而對於大多數程序來說,其使用到的空間遠未達到 4GB,何必去映射不可能用到的空間呢?也就是說,一級頁表覆蓋了整個 4GB 虛擬地址空間,但如果某個一級頁表的頁表項沒有被用到,也就不需要創建這個頁表項對應的二級頁表了,即可以在需要時才創建二級頁表。做個簡單的計算,假設只有 20% 的一級頁表項被用到了,那麼頁表佔用的內存空間就只有 0.804MB(1K * 4B + 0.2 * 1K * 1K * 4B = 0.804MB)。

除了在需要的時候創建二級頁表外,還可以通過將此頁面從磁盤調入到內存,只有一級頁表在內存中,二級頁表僅有一個在內存中,其餘全在磁盤中(雖然這樣效率非常低),則此時頁表佔用了8KB(1K * 4B + 1 * 1K * 4B = 8KB),對比上一步的 0.804MB,佔用空間又縮小了好多倍!總而言之,採用多級頁表可以節省內存。

二級頁表就是將頁表再分頁。仍以之前的 32 位系統爲例,一個邏輯地址被分爲 20 位的頁碼和 12 位的頁偏移 d。因爲要對頁表進行再分頁,該頁號可分爲 10 位的頁碼 p1 和 10 位的頁偏移 p2。其中 p1 用來訪問外部頁表的索引,而 p2 是是外部頁表的頁偏移。

在這裏插入圖片描述

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章