您當前的位置:首頁 > 舞蹈

一文幫小白搞懂作業系統之記憶體

作者:由 程式猿阿星 發表于 舞蹈時間:2021-06-04

前言

作業系統是一門比較難啃的課程,同時作業系統知識對開發者們來說是十分重要,相信各位在學作業系統的時候,有太多的抽象難以理解的詞彙與概念,把我們直接勸退,即使懷著滿腔熱血的心情學作業系統,不到 3 分鐘睡意就突然襲來。

所以本人想把自己的想法透過圖解+大白話的形式,產出作業系統系列文章,讓小白也能看懂,幫助大家快速科普入門

本篇開始介紹記憶體,記憶體在作業系統中還是比較重要的,理解了它,對整個作業系統的工作會有一個初步的輪廓。

內容大綱

一文幫小白搞懂作業系統之記憶體

正文

什麼是記憶體

小故事

我們想去擺地攤(準備執行程式程序)需要經過那幾 個步驟,這裡猜測一下。

首先要去城管申請攤位(申請記憶體),城管(作業系統)根據現在剩餘的地毯空間與你地毯的規模劃分一塊相應大小的攤位(記憶體)給你,接著你就可以愉快的擺攤(執行程式程序)賺錢啦。

城管也會時不時的來檢查(整理記憶體空間碎片),攤位是否規整,有沒有阻礙正常的人行道。

簡而言之,電腦上的程式(程序)執行是需要使用到對應大小的物理記憶體。

虛擬記憶體

實際上執行的程序並不是直接使用物理記憶體地址,而是把程序使用的記憶體地址與實際的物理記憶體地址做隔離,即作業系統會為每個程序分配獨立的一套「

虛擬地址

」。

每個程序玩自己的地址,互不干涉,至於虛擬地址怎麼對映到物理地址,對程序來說是透明的,作業系統已經把這些安排的明明白白了。

作業系統會提供一種機制,將不同程序的虛擬地址和不同記憶體的物理地址對映起來,如下圖所示

一文幫小白搞懂作業系統之記憶體

由此我們引出了兩個概念:

程序中使用的記憶體地址叫

虛擬地址

存在計算硬體裡的空間地址叫

物理地址

簡單來說作業系統引入虛擬空間,程序持有的虛擬地址會透過 CPU 晶片中的記憶體管理單元(MMU)的對映關係,來轉換成物理地址,再透過物理地址訪問物理記憶體

作業系統是如何管理虛擬地址與物理記憶體地址之間關係?

主要有三種方式,分別是

分段、分頁、段頁

,下面我們來看看這三種記憶體管理方式

記憶體分段

程式包含若干個邏輯分段,如可由程式碼段、資料段、棧段、堆段組成,每個分段都有不同的屬性,所以記憶體以分段的形式把這些段分離出來進行管理

在記憶體分段方式下,虛擬地址和物理地址是如何對映的?

分段管理下的虛擬地址由兩部分組成,段號和段內偏移量

一文幫小白搞懂作業系統之記憶體

透過段號對映段表的項

從項中獲取到段基地址

段基地址+段內偏移量=使用的物理記憶體

透過上述知道了,使用段號去對映段表的項,使用項中的段基地址與偏移量計算出物理記憶體地址,但實際上,分段方式會把程式的虛擬地址分為4段,每個段在段表中有一個項,在這一項找到段的基地址,再加上偏移量計算出物理記憶體地址

分段的方式,很好的解決了,程式本身不需要關心具體物理記憶體地址的問題,但是它仍有不足之處:

記憶體碎片的問題

記憶體交換的效率低的問題

接下來對這兩個問題進行分析

分段方式是如何產生記憶體碎片的?

在說記憶體碎片之前,還是先弄明白,什麼是記憶體碎片?,8個人去外面吃飯,因為飯點原因,人比較多,剩下的都是4人小餐桌,這些4人小餐桌就是我們所說的記憶體碎片,此時會有小夥伴說,把2個4人小餐桌拼湊在一起就解決了這個問題,非常簡單,我們把這種方式稱為記憶體碎片整理(涉及到記憶體交換)。

回到正題,我們來看一例子,假設物理記憶體只有1GB (1024MB),使用者電腦上運行了多個程式:

瀏覽器佔用128MB

音樂軟體佔用256MB

遊戲佔用了512MB

這個時候我們關閉瀏覽器,剩餘物理記憶體1024MB -(256MB+512MB)= 256MB。但是這剩餘的256MB物理記憶體不是連續的,被分為了兩段128MB,導致沒有空間再開啟一個200MB的程式,如下圖所示

一文幫小白搞懂作業系統之記憶體

這裡的記憶體碎片問題共有兩點:

外部記憶體碎片,就是多個不連續的小物理記憶體空間,導致新的程式無法被裝載

內部記憶體碎片,程式所有的記憶體都被裝載進了物理記憶體,但是程式有部分的記憶體,可能不經常使用,造成記憶體的浪費

解決外部記憶體碎片的方法就是使用記憶體碎片整理

記憶體碎片整理透過記憶體交換的方式來實現,我們可以把音樂軟體佔用的256MB載入到硬碟上面去,再從硬碟讀取回來,但是讀取回來的位置不再是原來的位置,而是緊跟已經佔用的遊戲512MB後面,這樣兩個128MB的空閒物理記憶體就合併成了一個256MB的連續物理記憶體,於是新的200MB新程式就能被裝載進來

記憶體交換空間,在 Linux 系統裡,是我們常看到的 Swap 空間,這塊空間是從硬碟劃分出來的,用於記憶體與硬碟的空間交換。

分段方式為什麼記憶體交換效率低?

首先分段管理容易造成記憶體碎片,導致記憶體交換的頻率較高,因為硬碟的訪問速度比記憶體慢太多了,然後每次交換的時候,把一大段連續的記憶體寫入到硬碟,再又從硬碟讀取出來,如果交換的是一個佔記憶體空間很大的程式,這樣整個機器都會顯得卡頓,過程也很慢的,所以說分段方式記憶體交換效率低。

為了解決記憶體分段管理造成的記憶體碎片與記憶體交換效率低的問題,就出現了記憶體分頁

記憶體分頁

分段的好處是能產生連續的記憶體空間,但是會出現大量記憶體碎片與記憶體交換效率低的問題

先思考一下怎麼解決這兩個問題,記憶體碎片是由多個不連續的小物理記憶體空間造成,如果把這些不連續的小物理記憶體空間組合起來,是不是解決了這個問題?同樣的,記憶體交換的時候我們保證交換的資料小,是不是能提高記憶體交換的效率?

這個辦法就是記憶體分頁,分頁是把整個虛擬與物理空間切成一段段固定尺寸的大小,這樣一個連續並且尺寸固定的空間,我們叫頁,在 Linux 下,每一頁的大小為 4KB。(虛擬空間是指儲存一套虛擬地址的空間)

虛擬地址與物理地址是透過頁表來對映,虛擬空間內的虛擬地址一定是連續的,物理地址不一定,但可以透過連續的虛擬地址把多個不連續的物理記憶體組合使用。

一文幫小白搞懂作業系統之記憶體

而當程序訪問的虛擬地址在頁表中查不到時,系統會產生一個缺頁異常,進入系統核心空間分配物理記憶體、更新程序頁表,最後再返回使用者空間,恢復程序的執行。

分頁方式是如何解決記憶體碎片與記憶體交換效率慢的問題呢?

記憶體碎片的解決:

因為使用記憶體的單位變成固定大小的頁,所以每個程式的虛擬空間維護的也是連續的頁(虛擬地址),透過頁表再對映到物理記憶體頁,雖然對映的物理記憶體頁不連續,但是虛擬空間是連續的,可以讓它們組合起來使用,但這也只能解決外部記憶體碎片問題,沒有解決內部內碎片問題,因為每頁都有固定大小,可能某一頁只使用了部分,依然會造成一些浪費。

記憶體交換效率慢的解決:

之前說過,減少交換資料的大小,可以提高記憶體交換效率,分頁方式是這樣解決的,如果記憶體空間不夠時,作業系統會把其他正在執行的程序中的「最近沒被使用」的記憶體頁釋放掉,也就是載入到硬碟,稱為換出,一旦需要的時候再載入進來,稱為換入。所以一次性寫入硬碟的也只有一個頁或幾個頁,記憶體的交換效率自然就提升了。

分頁方式使載入程式的時候,不再需要一次性都把程式載入到物理記憶體中。完全可以在進行虛擬記憶體和物理記憶體的頁之間的對映之後,並不真的把頁載入到物理記憶體裡,而是隻有在程式執行中,需要用到對應虛擬記憶體頁裡面的指令和資料時,再載入到物理記憶體裡面去(用大白話說,當你需要用到的時候才會去使用對應的物理記憶體)。

在記憶體分頁方式下,虛擬地址和物理地址是如何對映的?

在分頁機制下,每個程序都會分配一個頁表,虛擬地址會分為兩部分,頁號和頁內偏移量,頁號作為頁表的索引,頁表包含物理頁每頁所在物理記憶體的基地址,頁內偏移量+物理記憶體基地址就組成了物理記憶體地址,如下圖所示

一文幫小白搞懂作業系統之記憶體

就是下面這幾步

頁號找到頁表中的頁項

獲取頁項的物理頁號基地址

偏移量+物理頁號基地址計算出物理記憶體地址

是不是非常的簡單,但是這種分頁方式使用到作業系統上會不會問題呢?那必然是會有問題的,還記得之前提到的每個程序會分配一個頁表嘛?下面來為大家解開這個伏筆

在分頁方式下,每個程序分配一個頁表會有什麼問題?

不賣關子了,每個程序分配一個頁表會有空間上的缺陷,因為作業系統上可以執行非常多的程序,那不就意味著頁表數量非常多!

1B(Byte 位元組)=8bit,

1KB (Kilobyte 千位元組)=1024B,

1MB (Megabyte 兆位元組 簡稱“兆”)=1024KB,

1GB (Gigabyte 吉位元組 又稱“千兆”)=1024MB

以32 位的環境為例,虛擬地址空間範圍共有 4GB,假設一個頁的大小是 4KB(2^12),那麼就需要大約 100 萬 (2^20) 個頁,每個「頁表項」需要 4 個位元組大小來儲存,那麼整個 4GB 空間範圍的對映就要有 4MB 的記憶體來儲存頁表。

4MB看起來不大,但是數量上來了就很恐怖了,假設 100 個程序的話,就需要 400MB 的記憶體來儲存頁表,這是非常大的記憶體了,更別說 64 位的環境了。

為了解決空間上的問題,在對分頁方式的基礎上,進行最佳化,出現了多級頁表方式

多級頁表

在前面我們知道了,分頁方式在32位環境下,以每頁4KB來計算,一共有100萬頁,「頁表項」需要 4 個位元組大小來儲存,一個頁表包含100萬個「頁表項」,那麼每個程序的頁表需要佔用4MB大小,多級頁表要如何解決這種問題呢?

在頁表的基礎上做一次二級分頁,把100萬「頁表項」分為一級頁表「1024個頁表項」,「一級頁表項」下又關聯二級頁表「1024個頁表項」,這樣一級頁表的1024個頁表項就覆蓋到了4GB的空間範圍對映,並且二級頁表按需載入,這樣頁表佔用的空間就大大降低。

做個簡單的計算,假設只有 20% 的一級頁表項被用到了,那麼頁表佔用的記憶體空間就只有 4KB(一級頁表) + 20% * 4MB(二級頁表)= 0。804MB,這對比單級頁表的 4MB 是不是一個巨大的節約?

一文幫小白搞懂作業系統之記憶體

接著思考,在二級的基礎上是不是又可以繼續分級呢,能分二級,必然也能分三級、四級,在64位作業系統是做了四級分頁,分為了四個目錄,分別是

全域性頁目錄項

上層頁目錄項

中間頁目錄項

頁表項

一文幫小白搞懂作業系統之記憶體

TBL

多級頁表雖然解決了空間上的問題,但是我們發現這種方式需要走多道轉換才能找到對映的物理記憶體地址,經過的多道轉換造成了時間上的開銷。

程式是區域性性的,即在一段時間內,整個程式的執行僅限於程式的某一部分。相應的,執行所訪問的儲存空間也侷限於某個記憶體區域。

作業系統就利用這一特性,把最多使用的幾個頁表項放到TBL快取, CPU 在定址時,會先查 TLB,如果沒找到,才會繼續查常規的頁表,TLB 的命中率其實很高的,因為程式最常訪問的頁就那麼幾個。

記憶體段頁

段式與頁式並不是相對的,他們也可以組合在一起使用,在段的基礎上進行分頁分級

先將程式劃分為多個有邏輯意義的段,也就是前面提到的分段機制

接著再把每個段劃分為多個頁,也就是對分段劃分出來的連續空間,再劃分固定大小的頁

虛擬地址結構由段號、段內頁號和頁內位移三部分組成

一文幫小白搞懂作業系統之記憶體

就是下面這幾步

透過段號獲取段表的段項

透過段項獲取到頁表地址

透過頁表地址找到段頁表

透過段內頁號找到段頁表的段頁項

透過段頁項獲取物理頁基地址

透過物理頁基地址+偏移量計算出物理記憶體地址

總結

程序並不是直接使用物理記憶體,而是透過虛擬地址對映使用,所以作業系統會為每個程序分配虛擬空間(一套地址),使得每個程序使用物理記憶體互不影響,相互隔離。

啟用大量程序造成記憶體緊張不足的時候,作業系統會透過記憶體交換技術,把不常使用的記憶體載入到硬碟(換出),使用時從硬碟載入到記憶體(換入)

作業系統對記憶體的管理方式分為三種,分段、分頁、段頁,分段的好處是物理記憶體空間是連續的,但是缺點很明顯,容易造成記憶體碎片,並且記憶體交換效率慢,採用分頁能很好的解決分段的缺陷,透過連續的虛擬地址解決了外部記憶體碎片問題,每次記憶體交換將最近不使用的記憶體以頁的單位換出換入,保證交換資料大小,提高記憶體交換效率,但是會有頁表空間佔用問題,為了解決此問題,在分頁的基礎上最佳化成多級分頁+TBL方式來減少空間佔用與時間消耗,最後一個就是段頁,段頁是分段與分頁的結合。

透過思考,我們發現,多級分頁透過樹+懶載入+快取解決了空間佔用與時間消耗的問題,虛擬地址很好的做到了讓程序與物理記憶體地址解耦,正因如此,多程序使用物理記憶體時才不會有衝突,很好的做到了相互獨立與隔離。

關於我

這裡是阿星,一個熱愛技術的Java程式猿,公眾號

「程式猿阿星」

裡將會定期分享作業系統、計算機網路、Java、分散式、資料庫等精品原創文章,2021,與您在 Be Better 的路上共同成長!。

非常感謝各位小哥哥小姐姐們能 看到這裡,原創不易,文章有幫助可以「點個贊」或「分享與評論」,都是支援(莫要白嫖)!

願你我都能奔赴在各自想去的路上,我們下篇文章見!

一文幫小白搞懂作業系統之記憶體