基本概念
基本硬件
CPU可以直接訪問的通用存儲只有內存和處理器的內置的寄存器。機器指令可以用內存地址作為參數,而不能用磁盤地址作為參數。所以執行指令以及指令使用的數據,應在這些可執行訪問的存儲設備上,如果數據不在內存中,那么在CPU使用他們之前應把數據移到內存上。
CPU內置寄存器通常可以在一個CPU時鍾周期內完成訪問,但是對於內存,完成內存的訪問可能需要多個CPU時鍾周期,這種結果造成的影響就是如果沒有數據用於完成正在執行的指令,那么CPU可能將會多次中斷(暫停)。所以需要在CPU和內存之間增加一個高速緩存。
為了確保進程的執行正確,需要為每個進程分配一塊單獨的內存空間,從而使進程內存空間保護進程而互相不受到影響。通過兩個寄存器,通常為基地址寄存器和界限地址寄存器。基地址寄存器中含有最小的合法的物理內存大小;界限地址寄存器中指定了進程范圍的大小。內存空間的保護實現是通過在CPU硬件對在用戶模式下產生的地址和寄存器的地址進行比較來完成的。也只有操作系統可以通過特殊的特權指令,才能加載基地址寄存器和界限地址寄存器,因為特權指令只能在內核模式下執行,所以只有操作系統可以加載基地址寄存器和界限地址寄存器。
地址綁定
通常程序是作為二進制可執行文件存放在磁盤上的,如果需要執行的話,首先要將程序調入內存並放在進程中。在磁盤上等待調入內存以便執行的進程形成了輸入隊列。
大多數情況下,用戶程序在執行前,需要經過好幾個步驟,在這些步驟中,地址可能會有不同的表示形式。源程序中地址通常使用符號表示,編譯器通常將這些符號地址綁定到可重定位的地址。鏈接程序或加載程序在將這些可重定位的地址綁定到絕對地址。每次綁定都是從一個地址空間到另一個地址空間的映射。
通常,指令和數據綁定到存儲器地址可在沿途的任何一步中進行:
- 編譯時:如果編譯時就已經知道進程將在內存中的駐留地址,那么就可以生成絕對代碼,如果將來開始地址發生變化,那么就有必要重新編譯代碼。
- 加載時:如果在編譯時不知道進程將駐留在何處,那么編譯器就應生成可重定位代碼。對這種情況,最后綁定會延遲到加載時才進行。如果開始地址發生變化,那么只需要重新加載用戶代碼以合並更改的值。
- 執行時:如果進程在執行時可以從一個內存段移到另一個內存段,那么綁定應延遲到執行時才進行。大多數通用操作系統就是采用這種方式的。
邏輯地址空間和物理地址空間
CPU生成的地址通常稱為邏輯地址,而內存單元看到的地址(即加載到內存地址寄存器的地址)通常是物理地址。編譯時和加載時的地址綁定方法生成相同的邏輯地址和物理地址,但是執行時的地址綁定方案生成不同的邏輯地址和物理地址。在這種情況下,通常成邏輯地址為虛擬地址。
從虛擬地址到物理地址的運行時映射是由**內存管理單元(MMU)**的硬件設備來完成。用戶進程所生成的地址在送交到內存之前,都將加上重定位寄存器的值(基地址寄存器)。而用戶程序不會看到真實的物理地址。
動態加載
在之前的進程加載到內存中,是將進程的整個程序和所有數據都加載到物理內存當中,所以進程的大小受限於內存的大小,為了獲得內存空間利用率,可以采用動態加載。采用動態加載時,一個程序只有在調用時才會加載,所有程序都以可重定位加載格式保存在磁盤中。主程序被加載到內存並執行,當一個程序需要調用另一個程序時,調用程序首先檢查另外一個程序是否已經加載到內存中。如果沒有,可重定位鏈接程序會加載所需的程序到內存中,並更新程序的地址表以反映這個變化,接着控制傳遞給新加載的程序。所以,動態加載的好處就是,只有一個程序需要時才會被加載。
動態鏈接
動態鏈接庫是系統庫,可鏈接到用戶程序,以便運行。
如果是靜態鏈接的話,它的系統庫與其他目標模塊一樣,通過加載程序被合並到二進制程序映像。但是動態鏈接庫類似動態加載(注意在這里不是加載而是鏈接),會延遲到運行時。
如果有動態鏈接,在二進制映像中,每個庫程序的引用都有一個存根。存根是一小段代碼,用來支出如何定位適當的內存駐留庫程序,或者在程序不在內存時應如何加載庫。當執行存根時,他首先檢查所需程序是否已經在內存中,如果不在,將程序加到內存中。存根會用程序地址來代替自己,並開始執行程序,所以下次在執行程序代碼的時候,就可以直接進行,而不會因動態鏈接產生任何開銷。
進程交換
進程必須在內存中以便執行,但是,進程可以短暫的從內存交換到備份存儲,當再次執行時在調回到內存中。
標准交換
標准交換在內存和備份存儲之間移動進程,備份存儲通常是快速磁盤。備份存儲應該足夠的大,以容納所有用戶的所有內存映像的副本,並且應提供對這些存儲器映像的直接訪問。系統維護一個可運行的所有進程的就緒隊列,他們的映像在備份存儲和內存中,當CPU調度器決定要執行一個進程時,它調用分派器。分派器檢查隊列中的下一個進程是否在內存中,如果不在且沒有空閑內存區域,那么分派器會換出當前位於內存的一個進程並換入所需進程,然后重新加載寄存器,並將控制權給所選進程。
移動系統的交換
移動系統通常不支持任何形式的交換,移動設備通常采用閃存,而不是空間更大的硬盤作為他的永久存儲。蘋果的IOS和谷歌的Android的具體交換策略可以自行百度。
連續內存分配
連續內存分配是早期OS所采用的一種內存分配策略,在采用連續內存分配時每個進程位於一個連續的內存區域,與包含下一個進程的內存相連。
內存保護
為了放置進程訪問不屬於他們的內存,依舊通過重定位寄存器和界限寄存器來實現保護,MMU通過動態的將邏輯地址加上重定位寄存器的值。當CPU調度器選擇一個進程來執行時,作為上下文切換工作的一部分,分派器會用正確的值來加載重定位寄存器和界限寄存器。由於CPU所產生的每個地址都需要與這些寄存器進行核對,所以可以保證操作系統和其他用戶的程序和數據不受該運行進程的影響。
內存分配
最簡單的內存分配方法,就是將內存分為多個固定大小的分區。每個分區可以只包含一個進程。所以多道程序的程度受限於分區數。
對於可變分區方法,操作系統維護一張表,用於記錄哪些內存可用和哪些內存已用。開始時所有內存都可用於用戶進程,因此可以作為一大塊的可用內存,稱為孔。最后內存有一個集合,以包含各種大小的孔。
通常可用的內存塊為分散在內存里的不同大小的孔的集合。當新進程需要內存時,系統為該進程查找足夠大的孔。如果孔太大,那么就分為兩塊:一塊分給進程,一塊返回孔集合。這種方法是通用動態存儲分配問題的一個例子。從一組可用孔中選擇一個空閑孔的最常用方法有:
- 首次適應:分配首個足夠大的孔,查找可以從頭開始,也可以從上次首次適應結束時開始。一旦找到足夠大的空閑孔,就可以停止.
- 最優適應:分配最小的足夠大的孔。應查找整個列表,除非列表按大小排序。這種方法可以產生最小剩余孔
- 最差適應:分配最大的孔。同樣應該查找整個列表,除非列表按大小排序這種方法可以產生最大剩余孔。
碎片
用於內存分配的首次適應和最優適應算法都會有外部碎片。對於內存的碎片可以是內部碎片,也可以是外部的。比如假設有一個18464字節大小的孔,有一個進程需要18462字節,如果只能分配所要求的塊,那么還剩下2字節的孔。因此通常按固定大小的塊為單位來分配內存,采用這種方法,進程所分配的內存可能比所需要的大,這兩個數字之差稱為內部碎片,這部分內存存在於分區內部,但是又不能用。
外部碎片的一種解決方法是緊縮,移動內存內容,以便將所有的空閑空間合並成一整塊。但是緊縮並不是總是可能的。如果重定位是靜態的,並且在匯編時或加載時進行的,那么就不能緊縮;只有重定位是動態的,且在運行時進行的,那么才可以采用緊縮。