任何新技術都是在一點一點的積累中成熟並呈現在世人的面前,就像猿人進程成人也不是一簇而就的,而是在漫長的歲月中一點一點的進化與完善。還比如現代的吸塵器,當前發明吸塵器的那個人只是用了一台風扇的電機和葉片以及一個布口袋制成的,后來也是一點一點的改善成為現在的使用方便的吸塵器。
操作系統的內存管理也是同樣的道理,起初的操作系統並沒有現代操作系統的虛擬內存管理機制,而是指令直接訪問物理內存,並且在內存中同一時間只能運行一個進程,因為如果是多個進程,進程A中的指令有可能會修改進程B的內存地址,造成進程B崩潰或錯誤的計算結果。
后來開始支持多任務也就是同時可以運行多個進程,為每個進程都分配一塊獨立的內存地址並寫有保護位,其它進程指令是無法訪問當前進程的地址空間的,並且利用上了cpu的基址寄存器和界限寄存器來表達當前進程的物理地址訪問開始端和范圍,開始支持程序指令以相對內存地址運行。支持多任務固然是好,讓cpu的利用率大大提高,提高了工作效率,同時讓程序以相對地址訪問更加解放了程序員的工作。可是多進程帶來的問題就是內存空間大小的局限性,就算有1G內存空間,如果我的程序加上正文加上數據算100M,也就最多能運行10個,這還不算操作系統占用的,所以人們后來就發明了交換技術,當內存無法同時容納多個程序是,就根據策略比如很久以后才會運行的進程,就將程序的正文還有堆棧以及程序計數器這些進程映像存儲到硬盤上,當運行時再次裝入內存,恢復寄存器和程序計數器,這樣雖然解決了內存空間有限的問題,但是也帶來了新的問題,程序數據在內存與硬盤上的往復交換帶來了大量的io讀寫磁盤操作,大大降低了程序的運行效率。
經過以上的兩個階段的問題與整理,人們終於走上了虛擬內存的道路,現代的大多操作系統內存管理都采用分頁和分段的內存管理機制,所以下面我們詳細的說一下機制。
所謂虛擬內存,簡單的來說就是程序指令訪問的內存地址不是真的內存的物理地址,而是需要一個轉換過程才能訪問到物理地址,讀取或存取數據。
在操作系統中讓運行的每個程序擁有自己的空間地址,這個地址被分成多個塊,每個塊被稱為一頁或“頁面”,每一頁有連續的虛擬內存地址,每一個頁都對應物理內存中的一塊,這種塊被稱為“頁框”,頁框和頁面同等大小,頁面與頁框的對應關系被稱為頁表存儲在進程的地址空間中。當指令訪問一個內存地址是,經過硬件(cpu中的mmu,負責虛擬地址轉換,每一個程序運行時,就會將頁表信息設置mmu,讓mmu知道頁面與頁框的關系,從而快速計算出實際物理地址)快速計算出實際的物理地址,讀取地址獲取數據,如果發現訪問的虛擬地址不存在,則有操作系統負責將缺失的頁面由磁盤裝如內存,並更新頁表中頁面與頁框的對應關系。
頁表中存儲着頁面與頁框的映射關系,當cpu發出一條訪問內存的指令,mmu會根據內存地址的前幾位在頁表中找出對應的頁面與頁框的記錄,從而知道了頁框的地址,采用地址的后幾位作為物理地址區間頁框的偏移量,這樣找到了頁框並且知道了該頁框上的位置,就知道了訪問的真實的內存地址。
頁表中還保存了一些保護位。第一個是狀態位,就是頁面是否在內存中,如果存在則根據頁框與頁面的對應關系找出物理地址,如果不存在,則引起缺頁中斷有操作系統將缺失頁面裝入內存並更新頁表。第二則是保護位,記錄只讀或可讀寫,第三是記錄頁面修改情況,以便在將頁框中的信息置換到硬盤時寫入磁盤還是直接丟棄。第四個是禁止高速緩存位,如果操作系統一直在讀取軟盤上的數據,保證cpu讀取到的數據只是最新的是很有必要的,所以要設置這個位。
當頁框也就是物理內存不夠使用的時候,操作系統會根據一些頁面置換算法將內存中的數據置換到磁盤上的交換空間(swap),騰出空閑的頁框來存儲需要在內存中運行的程序和相關數據。
在所有的頁面置換算法中,以老化算法應用最廣泛和最實用,下面大概說一下這個頁面置換算法。
在cpu剛使用的頁面很可能在后面的幾條指令中被使用,反過來說,已經很久沒有使用的頁面在未來的一段時間很可能仍然不被使用,因為程序指令是順序執行的,當然存在在jmp這樣的跳轉,可是大多數情況,程序指令還是以此來執行的。老化算法就是根據這個思想來實現的,在發生缺頁中斷時,置換很久沒有使用的頁面,這個策略被稱為LRU。
老化算法的大概實現思路是,在內存中存儲一個數據結構,里面存儲頁面的使用次數,每次時鍾滴答后,將內存中頁面的引用次數加到數據結構里面的數值上,每次先將數值右移一位(1000=>0100)然后將本次使用設置到數值的最左邊((使用)1100 不使用0100),然后在發生頁面替換時,則替換數值最小的那個也就是使用次數最小的.
下面說一下操作系統中發生缺頁中斷時的處理流程
1.當前進程的某條指令被放入cpu,cpu發出指令訪問內存,該地址不存在,系統則陷入內核。
2.內核首先存儲當前進程的堆棧寄存器以及程序計數器來保存當前進程的進度印象,為處理完缺頁中斷后繼續運行當前程序做准備。
3.操作系統根據cpu中寄存器發現發生缺頁中斷的內存地址。如果寄存器中沒有,則必須檢索上一部保存的程序計數器來發現發生缺頁中斷的虛擬內存地址
4.發現了需要處理的地址后,系統檢查這個地址是否有效,是否越界。如果不符合檢查條件,則殺掉該進程,繼續運行別的進程。當符合條件。則系統檢查是否有空閑頁框,如果不存在空閑頁框,則調用頁面置換算法尋找一個頁面調換出去,為此頁面騰出一個頁框。
5.發現需要置換的頁面后,則檢查該頁面是否屬於臟頁面也就是數據裝入內存后被修改過,則需要調用程序將此數據寫回磁盤,並發生一次上下文切換,掛起當前處理缺頁中斷的進程,讓其他進程繼續運行。
6.當頁面干凈后,則調用io程序將數據從磁盤寫入內存,並掛起當前進程,讓系統繼續調度其他待運行進程。
7.數據裝入頁框后,掛起進程被喚醒。系統將程序計數器以及各個寄存器以及堆棧恢復到cpu和產生缺頁中斷的進程空間地址中,繼續運行產生缺頁中斷的進程。
在操作系統中,一個進程包含數據,程序正文,還有堆棧,這些數據都放在一個進程地址空間中,堆棧會隨着程序的執行變大和變小,數據也會跟着程序的進度而變化,當將這些不同類型的數據都放在一起時管理起來是很復雜的,比如堆棧的變大需要更多的內存空間時,是發生缺頁中斷還是增加進程地址的內存空間。其實作為程序員的我們沒有必要關心這個,這些完全可以交由系統去做,故現代的操作系統采用了分段技術,就是將一個程序的進程地址空間分為 數據段 程序指令正文段 堆棧段 還有全局常量,將這些不同類型的數據放入不同的空間並維護自己的頁表,這樣的幾個獨立的數據空間,空間擴容或減小都有操作系統攜帶分頁程序來管理,程序員不需要關心這些。
總結:我們大概說了一下操作系統的內存管理機制,其實無非就是通過各種手段去調度交換,讓有限的資源得到最大程度的利用。分頁處理或分段處理也是通過分頁程序或分段程序,將有限的內存空間放入最需要的數據。這也是人們在一步一步發展中總結出來的解決問題的辦法的展示。