前言
操做系統是一門比較難啃的課程,同時操做系統知識對開發者們來講是十分重要,相信各位在學操做系統的時候,有太多的抽象難以理解的詞彙與概念,把咱們直接勸退,即便懷着滿腔熱血的心情學操做系統,不到 3 分鐘睡意就忽然襲來。數據庫
因此本人想把本身的想法經過圖解+大白話的形式,產出操做系統系列文章,讓小白也能看懂,幫助你們快速科普入門瀏覽器
本篇開始介紹內存,內存在操做系統中仍是比較重要的,理解了它,對整個操做系統的工做會有一個初步的輪廓。緩存
內容大綱
正文
什麼是內存
小故事
咱們想去擺地攤(準備運行程序進程)須要通過那幾 個步驟,這裏猜想一下。bash
首先要去城管申請攤位(申請內存),城管(操做系統)根據如今剩餘的地毯空間與你地毯的規模劃分一塊相應大小的攤位(內存)給你,接着你就能夠愉快的擺攤(運行程序進程)賺錢啦。網絡
城管也會時不時的來檢查(整理內存空間碎片),攤位是否規整,有沒有阻礙正常的人行道。分佈式
簡而言之,電腦上的程序(進程)運行是須要使用到對應大小的物理內存。優化
虛擬內存
實際上運行的進程並非直接使用物理內存地址,而是把進程使用的內存地址與實際的物理內存地址作隔離,即操做系統會爲每一個進程分配獨立的一套「虛擬地址」。操作系統
每一個進程玩本身的地址,互不干涉,至於虛擬地址怎麼映射到物理地址,對進程來講是透明的,操做系統已經把這些安排的明明白白了。計算機網絡
操做系統會提供一種機制,將不一樣進程的虛擬地址和不一樣內存的物理地址映射起來,以下圖所示code
由此咱們引出了兩個概念:
- 進程中使用的內存地址叫虛擬地址
- 存在計算硬件裏的空間地址叫物理地址
簡單來講操做系統引入虛擬空間,進程持有的虛擬地址會經過 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 的路上共同成長!。
很是感謝各位小哥哥小姐姐們能 看到這裏,原創不易,文章有幫助能夠「點個贊」或「分享與評論」,都是支持(莫要白嫖)!
願你我都能奔赴在各自想去的路上,咱們下篇文章見!