為什么進程使用的內存尺寸(虛擬存儲)可以比物理內存還大?
為什么一個進程所需的存儲空間大小能超過物理內存的大小?操作系統是如何管理機器上運行的多個進程的內存的?進程間共享存儲是如何做到的?通過top命令查看的VIRT和RES指標有什么不同?所有這些問題都跟虛擬存儲這個概念相關,虛擬存儲是計算機系統的重要概念,可以說,理解好虛擬存儲便是掌握了內存管理的鑰匙。
- 進程是執行中的程序,一個進程就是一個執行中的程序實例,同一個程序可以有多個執行的實例,對應多個進程。
- 系統上同時運行的多個進程共享機器的CPU和存儲資源,每個進程(線程)有一個獨立的邏輯執行流,它提供一種假象,好像在獨占的使用處理器;同時,每個進程有一個私有地址空間,這提供另一個假象,它好像在獨占的使用存儲器。
- 虛擬存儲是一層抽象,它為每個進程提供了一致的、私有的地址空間,借助這一層關鍵抽象,計算機科學中最深刻最成功的概念“進程”才得以實施。
- 虛擬存儲簡化了存儲器管理,鏈接器以獨立于內存物理地址的方式構建可執行程序。
虛擬存儲空間被分為內核空間和用戶空間兩部分,并非所有虛擬地址空間都會分配物理內存,只有實際使用的才會分配物理內存,這也體現在通過top命令查看進程的VIRT和RES兩項指標的數值差異上。
- 物理內存可視為一個巨大的字節數組,每個元素占用一個字節,有自己的獨立編號(也就是地址),這個地址叫物理地址(Physical Address, PA)。
- 物理內存資源被運行在系統中的所有進程共享,就像CPU資源被運行在系統中的所有進程/線程共享一樣。
- 應用程序中所使用的地址叫虛擬地址(Virtual Address, VA),每個進程都擁有獨立的私有虛擬地址空間,進程之間的虛擬存儲是隔離的,既進程A不通過特殊手段無法訪問進程B的某個虛擬地址。
- 虛擬地址在訪問真正的物理內存的時候,需要被轉換為物理地址, 虛擬地址到物理地址的轉換過程叫地址翻譯。
- CPU硬件和操作系統合作完成地址翻譯,CPU芯片上的存儲管理單元(MMU)查詢內存中的頁表動態完成地址翻譯,而頁表的內容由操作系統管理維護,這項工作由底層系統默默完成,對應用程序透明。
- 進程X和進程Y中的同一虛擬地址會被映射到不同物理地址,多個進程也可以通過共享存儲技術(Shared Memory)映射到同一塊物理內存。
- 32位系統的虛擬地址范圍是[0-2^32],總共4G Byte,這個地址的集合叫地址空間,64位系統擁有更大的地址空間,但不是2^64。
雖然進程擁有如此大的虛擬地址空間,但它通常不會真正占用這么大存儲空間。
系統以頁為單位管理存儲,32位系統上,PageSize通常為4K字節,64位系統上,PageSize通常為8K字節。
物理存儲器(內存)被以PageSize為單位分割為物理頁(Physical Page, PP),[0-4096)的連續空間被分為第1頁,[4096,8192)的連續空間被分為第2頁,以此類推,PageSize大小的物理頁也被稱為頁幀。
虛擬存儲空間也被按同樣的PageSize分割成虛擬頁(Virtual Page, VP),虛擬頁分為已分配和未分配兩種狀態,而已分配的頁又被細分為未緩存和已緩存兩種狀態。
進程的所有已分配的虛擬存儲頁構成進程的有效虛擬存儲空間,對未分配的虛擬存儲空間的訪問非法,將觸發異常(例如常見的段錯誤)
通過調用malloc/new/mmap等編程接口分配虛擬存儲頁,對物理內存頁的分配由系統負責。
分頁后,一個虛擬地址被分為2部分:頁編號 + 頁內偏移。
操作系統在內存中為每個進程維護一個頁表(PageTable, PT), 地址翻譯硬件通過查詢存放在物理存儲器中的頁表,來找到對應的物理地址。
每個虛擬頁對應一個頁表條目(Page Table Entry, PTE),頁表視為頁表條目的數組,頁號就是數組下標(索引)。
每個頁表條目包含該虛擬頁是否已分配,對未分配的頁的訪問非法,如果已分配,則又要區分是否已緩存(對應到物理內存頁)和未緩存。
如果已緩存(頁命中),則頁表條目包含該虛擬頁對應的物理地址;如果未緩存(頁命失),則頁表條目包含該虛擬頁對應磁盤上該虛擬頁的起始位置,系統在物理存儲器中挑選一個犧牲頁,并將虛擬頁從磁盤拷貝到內存,這個過程叫換頁(swapping)
犧牲頁的內容需要從內存拷貝到磁盤虛擬頁,這個過程叫換出(swap out),被緩存的新頁需要從磁盤拷貝到內存,這個過程叫換入(swap in),因為牽扯到磁盤操作,過程中,一直等待,成本很高,這個成本被稱為命失懲罰,但根據局部性原理,程序經歷啟動階段的初始后,通常只會在一個較小的活動頁面集上工作,這便能保證,雖然懲罰的成本看似很高,但實際上,它依然工作的很好。
通過頁面調度,我們的程序能夠在超過物理內存容量的虛擬存儲空間下工作,且多個進程間,能有效的共享稀缺的內存資源。
理解這個過程對掌握內存管理和優化技術至關重要。
linux進程虛擬存儲空間
linux進程的虛擬存儲空間自底向上分為:代碼段、全局變量段、堆、共享存儲區、棧、內核段。
程序啟動時,加載器會將編譯后的可執行程序文件中的.text節拷貝到代碼段,.data拷貝到全局變量段,每個函數(內聯除外)編譯后都會占存儲空間,進文本節,程序執行中,會從代碼段源源不斷的加載指令。
堆向上生長,brk指向堆頂,通過malloc/new等編程接口從堆分配內存,動態分配的內存塊需要手動釋放。
棧向下生長,局部變量位于棧中,函數調用時的參數也經棧傳遞,函數調用鏈所需內存由棧提供,棧是自動伸縮的,每個線程會有獨立的棧,每個棧的空間有限(4/8M,可調節),所以不能局部變量不能過大,遞歸過深有爆棧分險。
堆內存和棧內存本質上都是存儲區,位于進程的不同區段,只有使用方式上的不同,沒有物理介質上的不同,都會經地址翻譯到物理內存。
棧和堆之間是共享存儲區,通過共享存儲編程接口shmget創建的存儲段位于該段,標準庫的代碼在進程間也被共享,這樣能夠節省內存。
內核段存儲空間,用戶態代碼不可訪問,但用戶代碼調用系統調用、或者觸發異常,進程陷入內核,會執行內核段的代碼+訪問內核態數據。
注意:代碼段并非從0開始,而是從特定地址開始,0x8048000(32位系統),0x400000(64位系統)
段頁式內存管理
早期計算機系統,采用段頁式的內存管理,既有分頁,又有分段,段內包含頁,但linux系統簡化了這個管理方式,進程的虛擬地址空間只有1段,所以相當于變相的廢棄了分段。
如果以4K為一頁,那么32位系統,虛擬內存空間為4G(2^32),因為頁表是進程私有,所以,一個進程的虛擬地址空間包含1M頁,需要1M頁表項(64位更多),而系統中經常成百上千個進程,這個內存占用量太大了,所以,實際上,操作系統使用多級頁表巧妙的解決了這個問題。
多級頁表是壓縮頁表內存占用的慣用法,引入多級頁表后,頂級頁表的一個表項不再表示4K/8K的地址范圍,它大的多,只有一級表項表示的范圍被分配,其對應的2級頁表才會被存儲,所以極大的節省了內存空間,而因為進程實際分配的虛擬頁,只是整個地址空間的很小一部分,所以,我們得以以小的存儲代價,支撐很大的地址范圍。
每次地址翻譯,都需要查詢內存頁表,如果頁表條目不在緩存中,則開銷很大,為了加快內地翻譯速度,便引入了TLB(翻譯后備緩存),TLB是MMU中的一個PTE的小的緩存,MMU在走地址翻譯的時候,先從TLB查找PTE,失敗了再去PageTable里找,還是因為局部性,TLB極大的加速了地址翻譯的過程。
軟硬件協作
- 地址翻譯,需要操作系統、MMU(TLB)協作完成
- 操作系統為每個進程維護頁表
- MMU先查TLB緩存,沒找到,再查頁表
- 進程調度后,頁表基址寄存器會修改指向新的運行進程的頁表其實地址
page fault
- page fault:實際上并不是真正的錯誤,沒有名字看起來這么嚴重,指令執行時,引發缺頁(page fault)會觸發異常,操作系統的異常處理程序會妥善處置這個異常,并再次發射這個指令,這時候,因為請求的地址已經被裝載到內存,指令得以正常進行。
- major page fault: 也叫hard page fault,major page fault主要是由swapping機制引入的,因為牽扯到磁盤io,主要由軟件完成,所以速度較慢,可通過swapon/swapoff開關系統交換分區,也可以設置修改系統設置:swappiness
- minor page fault:也叫soft page fault,指需要訪問的代碼/數據已經在物理內存頁中,但頁表中的映射還沒有建立,需要MMU建立物理內存和虛擬地址空間的映射關系。當一個進程在調用malloc獲取虛擬空間地址后,首次訪問該地址會發生一次soft page fault。多個進程訪問同一個共享內存中的數據,當某些進程還沒有建立起映射關系,訪問時也會出現soft page fault。
可以通過命令:ps -eo min_flt,maj_flt,cmd 查看系統各個進程的page fault情況。
當程序通過malloc分配1G字節的內存時,系統并不會為進程分配1G的物理內存,而只是分配1G的虛擬內存頁,當之后真正訪問某個頁的時候,系統才會為它分配物理內存,如果系統物理內存被耗盡,則會挑選一個內存頁,交換出去(把頁的內容寫入磁盤頁),通過這樣的策略,我們能獲得比物理內存更大的存儲空間,提高了內存資源的利用率,也使得進程之間共享存儲變得可能。