虛擬存儲器,你了解嗎?
首先說為什麼要有虛擬存儲器
我個人覺得虛擬存儲器的概念是和進程概念一起出現的。在計算機技術發展的早期,只有單道批處理系統,特點是一次只能運行一個進程,只有運行完畢後才能將下一個進程載入到內存裡面,所以進程的數據都是直接放在物理內存上的。
到後來發展出了多道程序系統,它要求在計算機中存在著多個進程,處理器需要在多個進程間進行切換。這時候就出現問題了,鏈接器在鏈接一個可執行文件的時候,總是默認程序的起始地址為0x0,但物理內存上只有一個0x0的地址呀?也許你會說:」沒關係,我們可以在程序裝入內存的時候再次動態改變它的地址.」好吧我忍了。但如果我的物理內存大小只有1G,而現在某一個程序需要超過1G的空間怎麼辦呢?你還能用剛才那句話解釋嗎?
這時候虛擬存儲器的作用就發揮出來了,我們為每一個進程分配一個0~232-1個位元組(32位系統,下同)的虛擬地址空間,並將這些空間在邏輯上分為各個段,每個段的作用、位置和訪問許可權都不同,具體可見我的關於進程這篇文章。使用虛擬存儲器有如下好處:
- 方便程序的鏈接和裝入工作
- 裝入內存後可極大的節省內存
- 方便操作系統對進程的管理
- 安全,一個進程無法訪問其他進程的空間
- 可以保證每個進程可用空間為CPU的最大定址空間
- 可以高效的在進程間共享數據,如共享內存
- 其他的想起來在補充哈,反正好處很多的。。。
虛擬存儲器的工作原理
當操作系統將一個程序載入內存時,會為其創建一個PCB出來,PCB在Linux系統中就是一個task_struct的內核結構體,其中的元素包含或者指向內核運行該進程所需要的所有信息(例如:PID,指向用戶棧的指針,可執行目標文件的名稱以及程序計數器)。
task_struct中有一個條目mm指向mm_struct,它描述了虛擬存儲器的當前狀態。我們感興趣的兩個欄位是pgd和mmap,其中pgd指向一級頁表的基址,而mmap指向一個vm_area_structs的鏈表,其中每個vm_area_structs都描述了虛擬地址空間的一個區域,也就是我上文所說的「段」。示意圖如下:由上圖可知,vm_area_struct結構體中各欄位含義如下:
- vm_start:指向這個區域的起始處
- vm_end:指向這個區域的結束處
- vm_prot:描述這個區域內所有頁的讀寫許可權
- vm_flags:描述這個區域內的頁面是與其他進程共享的還是私有的
- vm_next:指向鏈表中下一個區域結構
值得說明的是,上圖中的用戶棧由兩個寄存器ebp和esp維護,堆由程序員自己維護,所以沒有vm_area_struct結構體指向它(個人認為,有待證實)。
以上數據結構創建好後,進程就可以載入運行了。當然,此時物理內存上還沒有任何該進程的數據信息。當CPU要訪問某一個地址時,發現該頁面並沒有存在於物理內存上,就產生一個缺頁中斷(關於頁表和缺頁中斷在下文表述),此時在中斷處理程序內,操作系統會開闢一塊內存出來並將外存上的數據存放進來,然後退出中斷處理程序,CPU重新運行剛剛產生中斷的那句指令,此時就不會再次導致缺頁了。
以上就是虛擬存儲器的工作原理。由此可見,根據程序運行的局部性原理,使用虛擬存儲器只將進程中用到的數據載入進物理內存,可以大大提高內存的使用率。
頁表和地址翻譯
既然有了虛擬地址,我們就需要有一種方法將虛擬地址和物理地址映射起來,這是通過頁表來實現的。頁表也是存儲在物理內存中的數據,只不過它由內核維護。頁表中記錄了一個虛擬地址是否已經被映射到物理內存上的某個位置,如已經映射,還記錄了具體的物理內存地址。由於訪問頁表相當於多了一次內存訪問,因此有的計算機系統將頁表緩存到MMU(Memory Manage Unit,內存管理單元)中的頁表緩存中,稱作TLB(Translation Lookaside Buffer,翻譯後備緩衝器)。當然,這不是我們要討論的內容。
當CPU需要訪問一個虛擬地址時,首先用這個虛擬地址根據某一個hash演算法去查找對應的頁表,這個操作的時間複雜度為O(1).若發現該虛擬地址已經被映射到物理內存上,則根據頁表中給出的物理地址再去物理內存上查找即可。接下來我們討論的是虛擬地址沒有被映射到物理內存的情況,即缺頁。
發生缺頁時,操作系統觸發一個缺頁異常,執行以下處理動作:
- 若頁表中還有空餘空間,則分配一個出來,同時分配一塊物理內存出來,將所需的數據從磁碟拷貝到這裡並更新頁表。
- 若頁表已滿,則根據某種頁面調度演算法選擇一個犧牲頁,在採用「寫一次法」的系統中,若該犧牲頁已經被修改,則將它同步回磁碟。然後再從磁碟將數據拷貝到一塊新分配的物理內存中並更新頁表。
- 退出異常處理程序時,CPU重新執行剛才導致缺頁中斷的那條指令。
有了以上關於頁表的基礎,我們在來看看以前學習的一些知識。
再看fork函數
fork函數的作用是創建一個子進程出來。其需要執行的動作有:
- 創建新進程的PCB,並將父進程的PCB中大部分欄位拷貝給子進程。
- 創建子進程的頁表,並為其分配實際物理內存,包括用戶區的所有段。
因此可以看出,創建一個子進程的開銷是很大的。現代操作系統採用了一種寫時拷貝的技術(COW,Copy On Write),即只是拷貝子進程的頁表,並沒有為其分配實際物理內存,也就是父子進程共同使用相同的物理內存。但會把這塊內存的vm_area_struct結構體中的vm_prot欄位標記為只讀的。當父子進程都讀取這些內存中數據時沒有問題,如果某一個進程往裡面寫數據,才開始為其分配實際物理內存,並將數據拷貝過去,將他們標記為可寫的,然後再寫入數據。
再看exec函數
exec是一個函數族,完成的功能是程序替換。需要以下幾個步驟:
- 刪除已存在的用戶區域。即將當前進程的代碼段,數據段等刪除掉。
- 映射私有區域。即將新的要替換的程序的代碼和數據段映射的當前進程虛擬地址空間的代碼和數據段,.bss段是請求二進位零的,映射到匿名文件,堆棧的初始長度為0。
- 映射共享區域。如果新的程序與共享對象鏈接,如C標準庫的libc.so,即動態鏈接,那麼這些庫文件映射到共享區域內。
- exec函數做的最後一件事情就是設置程序計數器的值,使之指向當前進程的入口點。
如上就是exec函數所做的工作,隨後Linux將根據需要換入代碼和數據頁面。
再看共享內存機制
Linux進程間通信機制中有一種方式是共享內存,其機理是使兩個進程的頁表指向同一塊物理內存,這樣兩個進程就可以通過頁表訪問同一塊內存了。
再看malloc函數
malloc和free是C標準庫的庫函數,用來在堆上分配和釋放空間。malloc管理著一個空閑鏈表,這個空閑鏈表記錄著堆空間上所有未被使用的空間,每次調用malloc函數時,就從該鏈表上找出一塊足夠大的空間返回給調用者並將其從空閑鏈表上刪除。如果找不到足夠大的空間,malloc就調用sbrk函數來增大brk變數,以增大堆空間。brk是內核為每個進程所維護的一個變數,記錄著堆的邊界。
free函數則與malloc相反,它直接將所分配的內存添加到空閑鏈表當中即可。但要注意的是,free函數必須要檢查所釋放的空間是否是由malloc/calloc/realloc函數分配的,如果不是,則觸發一個異常。推薦閱讀:
※提高 Linux 開發效率的 5 個工具
※系統調用的實現細節(用戶態)
※windows 10「詭異」的「Internet臨時文件」。
※系統突發性地磁碟佔有100%,資源管理器無限重啟
※你需要熟練運用的12個命令行工具