段與頁結合的初步思路
-
虛擬內存的引入:
-
為了結合段和頁的優勢,操作系統引入了虛擬內存的概念。虛擬內存是一段地址空間,它映射到物理內存上,但對用戶程序是透明的。
-
-
段到虛擬內存的映射:
-
用戶程序中的段首先映射到虛擬內存的相應區域。這一步驟通過段表(Segment Table)實現,段表記錄了每個段的起始地址和長度。
-
-
虛擬內存到物理內存的映射:
-
虛擬內存中的區域再映射到物理內存的頁上。這一步驟通過頁表(Page Table)實現,頁表記錄了虛擬頁到物理頁框的映射關系。
-
-
地址翻譯:
-
用戶程序發出的地址首先被翻譯成虛擬地址,然后再翻譯成物理地址。這一過程通常由硬件中的內存管理單元(MMU)自動完成。
-
-
內存訪問:
-
一旦地址被翻譯成物理地址,CPU就可以訪問物理內存中的數據,執行指令或進行數據操作。
-
用戶從應用程序視角看待虛擬內存中的段和頁
從用戶程序的視角來看,虛擬內存中的段和頁提供了一種抽象的內存模型,使得程序能夠以一種簡化和統一的方式來訪問內存。以下是用戶程序如何查看和與虛擬內存中的段和頁交互的方式:
-
邏輯地址空間:
-
用戶程序操作的是邏輯地址空間,這是一個由操作系統管理的虛擬地址空間。在這個空間中,程序可以使用段和偏移量(如
cs:ip
)來訪問內存,而不需要關心物理內存的實際布局。
-
-
地址翻譯:
-
用戶程序發出的地址(如
0x00345008
)首先被翻譯成虛擬地址,然后再由操作系統和硬件(如內存管理單元MMU)翻譯成物理地址(如0x7008
)。 -
這個過程對用戶程序是透明的,用戶程序不需要知道具體的物理地址。
-
-
內存保護和隔離:
-
段和頁的結合允許操作系統實現內存保護和隔離。每個進程都有自己的虛擬地址空間,操作系統確保一個進程不能訪問另一個進程的內存空間。
-
-
虛擬內存的優勢:
-
虛擬內存提供了內存的抽象,使得程序可以假設它擁有一個連續的、大的內存空間,而實際上物理內存可能是分散的、有限的。
-
這種抽象簡化了程序設計,提高了內存使用的靈活性,并允許操作系統更有效地管理內存資源。
-
重定位
邏輯地址到物理地址的轉換過程
-
邏輯地址:由段號和偏移量組成,例如
段號+偏移(cs:ip)
。 -
段表:包含段號、基址、長度和保護信息。
-
段號:標識段的編號。
-
基址:段在內存中的起始地址。
-
長度:段的大小。
-
保護:段的訪問權限(如只讀R、讀寫R/W)。
-
-
頁號和偏移:邏輯地址中的偏移量被進一步分解為頁號和頁內偏移。
-
頁表:包含頁框號和保護信息。
-
頁框號:頁在內存中的物理位置。
-
保護:頁的訪問權限(如只讀R、讀寫R/W)。
-
-
物理地址:由物理頁號和偏移量組成。
地址翻譯過程
-
邏輯地址:由段號和偏移量組成。
-
段表查找:根據段號查找段表,獲取基址和長度。
-
偏移分解:將偏移量分解為頁號和頁內偏移。
-
頁表查找:根據頁號查找頁表,獲取頁框號。
-
物理地址計算:將頁框號和頁內偏移組合成物理地址。
段頁同時存在的場景下,重定位過程是怎樣的
在斷頁同時存在的場景下,用戶發出的邏輯地址是CS加上段號和偏移量。
操作系統首先通過段表找到段在虛擬內存中的位置,并生成一個虛擬地址。
然而,這個虛擬地址并不是直接對應的物理內存地址,而是需要經過一次映射,根據虛擬地址計算出頁號,再結合頁內偏移得到物理地址。
最后,操作系統將這個物理地址發送到地址總線上,從而實現從段和頁兩個層次上的地址翻譯,確保代碼能夠正確執行和獲取數據。
一個實際的段頁結合的例子
內存分配
內存管理的核心
-
內存管理核心就是內存分配:強調了內存分配是內存管理的關鍵部分。
具體示例
-
指令:
_sum: .int 0
定義了一個名為_sum
的變量,初始值為0。 -
指令:
_main: mov [300], 0
將0移動到偏移量為300的地址。 -
地址計算:
base + 300(offset)
表示將基地址和偏移量相加得到物理地址。
內存管理過程
-
分配段、建段表:為進程分配內存段,并建立段表來管理這些段。
-
分配頁、建頁表:為進程分配內存頁,并建立頁表來管理這些頁。
-
進程帶動內存使用的圖譜:展示了進程如何使用內存的圖譜。
-
從進程fork中的內存分配開始:說明了進程創建(fork)時的內存分配過程。
載入內存
程序加載過程
-
分配段:為程序的不同部分(如代碼、數據、棧)分配內存段。
-
建立段表:記錄每個段的基址、長度和保護屬性。
-
分配頁:將每個段進一步分為頁,并為每頁分配物理內存。
-
建立頁表:記錄每個頁的頁框號和保護屬性。
-
地址轉換:將程序中的邏輯地址轉換為物理地址,以便訪問實際的內存。
示例
-
邏輯地址
0x300
:表示用戶數據段中的一個地址。 -
指令
_main: mov [300], 0
:將0移動到偏移量為300的地址。 -
段表和頁表:展示了如何通過段號和頁號查找對應的基址和頁框號,從而完成地址轉換。
分配虛擬內存,建立段表
從幻燈片中提取的代碼如下:
int copy_process(int nr, long ebp, ...)
{...copy_mem(nr, p); ...
}
?
int copy_mem(int nr, task_struct *p)
{unsigned long new_data_base;new_data_base = nr * 0x40000000; ?// 64M * nrset_base(p->ldt[1], new_data_base);set_base(p->ldt[2], new_data_base);
}
-
計算新的數據基址:
-
在
copy_mem
函數中,首先計算新的數據基址new_data_base
。這個基址是通過將進程編號nr
乘以0x40000000
(即64MB)來得到的。這意味著每個進程將獲得64MB的虛擬內存空間。
-
-
設置段表:
-
使用
set_base
函數將計算出的新數據基址設置到進程的局部描述符表(LDT)中的相應段。在這個例子中,p->ldt[1]
和p->ldt[2]
分別代表了兩個不同的段(例如,代碼段和數據段)。 -
set_base
函數的作用是更新段描述符中的基址字段,使其指向新的虛擬內存區域。
-
-
進程控制塊(PCB):
-
task_struct *p
是一個指向進程控制塊(PCB)的指針,它包含了進程的所有信息,包括段表。 -
在進程創建時,操作系統會為新進程分配一個新的PCB,并復制父進程的相關信息,同時為新進程分配獨立的虛擬內存空間。
-
-
內存分配:
-
在Linux中,每個進程都有獨立的虛擬內存空間。通過
fork()
系統調用創建的新進程會繼承父進程的虛擬內存布局,但是操作系統會確保新進程的虛擬內存空間是獨立的,以防止進程間的相互干擾。
-
-
進程切換:
-
在進程切換時,操作系統需要更新CPU的段寄存器,以指向新進程的段表。這樣,當新進程開始執行時,它將在自己的虛擬內存空間中運行。
-
分配內存,建立頁表
這張幻燈片展示了在Linux操作系統中,如何為新創建的進程分配內存并建立頁表。以下是提取的代碼和對分配內存、建立頁表過程的總結:
提取的代碼
int copy_mem(int nr, task_struct *p)
{unsigned long old_data_base;old_data_base = get_base(current->ldt[2]);copy_page_tables(old_data_base, new_data_base, data_limit);
}
?
int copy_page_tables(unsigned long from, unsigned long to, long size)
{from_dir = (unsigned long *)(((from >> 20) & 0xffc));to_dir = (unsigned long *)(((to >> 20) & 0xffc));size = (unsigned long)((size + 0x3fffff) >> 22);for (; size-- > 0; from_dir++, to_dir++) {from_page_table = (0xfffff000 & *from_dir);to_page_table = get_free_page();// 這里應該還有代碼來復制頁表項和設置新的頁表}
}
分配內存、建立頁表的總結
-
獲取舊數據基址:
-
在
copy_mem
函數中,首先獲取當前進程的數據段基址(old_data_base
),這通常是通過讀取當前進程的段表(LDT)來實現的。
-
-
調用復制頁表函數:
-
然后調用
copy_page_tables
函數,傳入舊數據基址、新數據基址(new_data_base
)和數據段的大小(data_limit
),以復制頁表。
-
-
計算目錄基址:
-
在
copy_page_tables
函數中,計算源目錄(from_dir
)和目標目錄(to_dir
)的基址。這是通過將虛擬地址右移20位(即頁目錄的索引)并取低12位(頁目錄項的偏移)來實現的。
-
-
計算頁表項數量:
-
計算需要復制的頁表項數量(
size
),這是通過將數據段大小加上0x3FFFFF(即1MB-1,因為頁表項是按頁大小為單位的),然后右移22位(即頁大小為4KB)來實現的。
-
-
復制頁表項:
-
在循環中,對于每個頁表項,從源目錄讀取頁表項(
from_page_table
),然后從頁框(page frame)中獲取一個空閑頁(to_page_table = get_free_page()
)來存儲目標頁表項。 -
這里應該還有代碼來復制頁表項的內容,并設置新的頁表項,包括頁框號、保護位等。
-
-
建立頁表:
-
通過上述步驟,為新進程建立了頁表,將虛擬地址映射到物理內存。
-
-
更新進程控制塊(PCB):
-
最后,需要更新新進程的PCB,包括新的頁表基址等信息,以便新進程可以正確地訪問其虛擬內存。
-
from_dir,to_dir
from_dir = (unsigned long *)(((from >> 20) & 0xffc));
to_dir = (unsigned long *)(((to >> 20) & 0xffc));
size = (unsigned long)((size + 0x3fffff) >> 22);
?
for (; size-- > 0; from_dir++, to_dir++) {from_page_table = (0xfffff000 & *from_dir);// 這里應該還有代碼來復制頁表項和設置新的頁表
}
-
計算頁目錄地址:
-
from_dir
和to_dir
是指向頁目錄的指針。它們是通過將虛擬地址右移20位(即頁目錄的索引)并取低12位(頁目錄項的偏移)來計算得到的。這是因為在x86架構中,頁目錄項是按4KB對齊的,所以需要取低12位來得到頁目錄項的偏移。
-
-
計算頁表項數量:
-
size
是需要復制的頁表項數量。這是通過將數據段大小加上0x3FFFFF(即1MB-1,因為頁表項是按頁大小為單位的),然后右移22位(即頁大小為4KB)來計算得到的。
-
-
復制頁表項:
-
在循環中,對于每個頁表項,從源目錄讀取頁表項(
from_page_table
),然后從頁框(page frame)中獲取一個空閑頁(get_free_page()
)來存儲目標頁表項。 -
這里應該還有代碼來復制頁表項的內容,并設置新的頁表項,包括頁框號、保護位等。
-
-
頁目錄指針:
-
頁目錄指針(CR3)用于指向當前進程的頁目錄。在進程切換時,需要更新CR3寄存器以指向新進程的頁目錄。
-
from_page_table,to_page_table
從幻燈片中提取的代碼和相關信息如下:
提取的代碼:
for (; size-- > 0; from_dir++, to_dir++) {to_page_table = get_free_page();*to_dir = ((unsigned long)to_page_table) | 7;
}
?
unsigned long get_free_page(void) {register unsigned long _res asm("ax");__asm__("std; repne; scasb\n\t""movl %%edx, %%eax\n\t""D"(mem_map+PAGING_END-1));return _res;
}
總結:
-
頁表復制過程:
-
在循環中,對于每個頁表項,首先通過調用
get_free_page()
函數為新進程分配一個空閑的頁框(物理內存頁)。 -
然后,將新分配的頁框地址設置到目標頁目錄項中(
to_dir
),并設置頁表項的權限(這里通過或操作| 7
來設置)。
-
-
頁表項權限設置:
-
在x86架構中,頁表項通常包含頁框號和一些權限位。這里的
| 7
操作可能是設置頁表項的權限位,例如可讀寫(RW)和存在位(P)。
-
-
頁表項結構:
-
幻燈片中展示了頁表項的結構,包括頁目錄號(10 bits)、頁號(10 bits)和偏移(12 bits)。這是x86架構中分頁機制的基本概念。
-
-
頁目錄和頁表:
-
from_dir
和to_dir
分別指向源進程和目標進程的頁目錄項。通過復制頁目錄項,可以實現頁表的復制和更新。
-
-
獲取空閑頁框:
-
get_free_page()
函數通過匯編語言實現,用于掃描內存映射(mem_map
)來找到一個空閑的頁框。這個過程涉及到檢查內存映射中的每個條目,直到找到一個空閑的頁框。
-
-
內存映射:
-
mem_map
是一個內存映射數組,用于跟蹤哪些頁框是空閑的,哪些已經被使用。PAGING_END
可能是定義了內存映射數組的結束位置。
-
復制和更新頁表
這張幻燈片展示了在操作系統中,如何復制和更新頁表項以實現內存管理。以下是提取的代碼和對過程的總結:
提取的代碼:
for (; nr-- > 0; from_page_table++, to_page_table++) {this_page = *from_page_table;this_page &= ~2; // 只讀*to_page_table = this_page;*from_page_table = this_page;this_page -= LOW_MEM;this_page >>= 12;mem_map[this_page]++;
}
總結:
-
頁表項復制:
-
代碼中的循環遍歷所有的頁表項,從源頁表(
from_page_table
)復制到目標頁表(to_page_table
)。
-
-
權限設置:
-
this_page &= ~2;
這行代碼通過位操作清除頁表項中的某個權限位(通常是只讀位),確保復制的頁表項具有適當的權限設置。
-
-
頁表項更新:
-
*to_page_table = this_page;
將修改后的頁表項寫入目標頁表。 -
*from_page_table = this_page;
也將修改后的頁表項寫回源頁表,這可能是為了確保源進程的頁表項也反映了權限的更改。
-
-
頁框號計算:
-
this_page -= LOW_MEM;
這行代碼從頁框號中減去一個基址(LOW_MEM
),可能是為了將頁框號轉換為相對于某個特定內存區域的偏移量。
-
-
頁框號轉換:
-
this_page >>= 12;
這行代碼將頁框號右移12位,這通常是為了將頁框號轉換為頁框數組中的索引。
-
-
內存映射更新:
-
mem_map[this_page]++;
這行代碼更新內存映射數組,增加對應頁框的使用計數。這是為了跟蹤每個頁框的使用情況,特別是在使用寫時復制(copy-on-write)技術時。
-
使用內存
寫時復制(COW)技術
寫時復制是一種優化技術,用于減少復制內存的開銷,特別是在創建新進程時。在寫時復制中,父子進程最初共享相同的物理內存頁,只有當進程實際修改內存頁時,才會創建該內存頁的副本。
幻燈片內容分析
-
內存共享與寫時復制:
-
只要段表和頁表設置正確,父子進程就可以通過MMU(內存管理單元)自動訪問相同的物理內存頁。
-
當父子進程中的任何一個嘗試修改共享的內存頁時,操作系統會觸發寫時復制機制,為修改內存頁的進程創建一個新的物理內存頁副本
-