References:
《操作系統真象還原》,鄭鋼
《x86彙編語言:從實模式到保護模式》,李忠
《彙編語言(第3版)》,王爽
=========================================================================================
Linux中90%以上的代碼都是用在資源管理、策略、算法及數據結構等方面。操作系統受制於硬件的支持, 很大程度上它的能力取決於硬件的能力,很多操作都是硬件自動完成的。比如,處理器進入0特權級時, 會自動在任務狀態段TSS中獲得0特權級的棧地址。因此,要想全面理解操作系統,不僅需要了解上層軟件的算法、原理、實現, 還要了解很多硬件底層的內容。
本項目實現的mini操作系統,包含:
1)內核線程、特權級變換、進程、任務調度、fork和exec、父子進程間的通信等;
2)內存管理、文件系統、管道、shell等;
3)鎖、信號量等。
=========================================================================================
BIOS
CPU的硬件電路被設計成只能運行內存中的程序(內存比較快)。
在開機加電的一瞬間,CPU的cs:ip寄存器被強制初始化爲0xF000:0xFFF0(BIOS入口地址),此處16字節的內容是跳轉指令"jmp f000:e05b"(BIOS程序起始地址)。BIOS的主要工作是:
1)檢測、初始化硬件,硬件自己提供了一些初始化的功能調用,BIOS直接調用;
2)建立中斷向量表IVT,這樣就可以通過"int 中斷號"來實現相關的硬件調用。這些功能的實現也是基於對硬件的IO操作。不過在保護模式下,中斷向量表已經不存在了,取而代之的是中斷描述符表IDT(Interrupt Descriptor Table)。
文本顯示:0xB 8000起始的32KB內存區域是用於文本顯示,往0xB 8000處輸出的字符會直接落到顯存中,顯存中有了數據,顯卡會自動將其搬到顯示器屏幕上。
; FILE: boot/mbr.asm
; 截取部分代碼
mov ax, 0xb800
mov gs, ax
; 打印字符串
mov byte [gs:0x00], 'M'
mov byte [gs:0x01], 0xa4 ; 顯示屬性
=========================================================================================
MBR
Master Boot Record, 主引導記錄, 位於0x7c00。0x7C00,是BIOS把MBR加載到內存後自動跳轉過去的地址。
功能:從硬盤指定位置處加載 loader, 並跳轉。
; FILE: boot/mbr.asm
; 截取部分代碼
LOADER_BASE_ADDR equ 0x900 ; 自定義loader被加載到物理內存位置
LOADER_START_SECTOR equ 0x02 ; 自定義loader位於硬盤的扇區號
; 讀取loader程序
mov eax, LOADER_START_SECTOR ; 起始邏輯扇區號,LBA28編址
mov bx, LOADER_BASE_ADDR ; 要寫入的目標地址
mov cx, 4 ; 要讀入的扇區數,這裏設爲4個扇區
call read_hard_disk_0
jmp LOADER_BASE_ADDR + 0x100 ; loader.bin文件頭部偏移256字節
times 510-($-$$) db 0
db 0x55, 0xaa
=========================================================================================
LOADER
功能:
1)調用BIOS中斷獲取內存大小;2)構建GDT,開啓保護模式;3)加載kernel;
4)構建頁目錄表和頁表,開啓分頁機制;5)解析kernel的ELF,將ELF文件中的段segment拷貝到各段自己被編譯的虛擬地址處;
6)跳轉
--------------------------------------------
1)調用BIOS中斷獲取內存大小
調用BIOS中斷0x15獲取內存大小,並將其值存放在 loader.bin頭部(地址0x900),內核將會從該位置讀取內存大小(kernel/memory.c mem_init())。
2)構建GDT,開啓保護模式
; FILE: boot/loader.asm
; 截取部分代碼
; 構建gdt及其描述符
; 0號段描述符
gdt_0: dd 0x0000_0000
dd 0x0000_0000
; 1號代碼段
gdt_code: dd 0x0000_ffff
dd DESC_CODE_HIGH4
; 2號數據段和棧段
gdt_stack: dd 0x0000_ffff
dd DESC_DATA_HIGH4
; 3號顯存段
; 基地址0xb_8000, 段大小0xb_ffff-0xb_8000=0x7fff, 粒度4KB, 段界限0x7fff/4k=7
gdt_video: dd 0x8000_0007 ; limit=(0xb_ffff-0xb_8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ; dpl爲0
; gdt基址和界限值
gdt_size dw $-gdt_0-1
gdt_base dd gdt_0
; 加載gdt
; lgdt指令的操作數是一個48位(6字節)的內存區域,低16位是gdt的界限值,高32位是gdt的基地址
; GDTR, 全局描述符表寄存器
lgdt [gdt_size]
; 打開地址線A20
; 芯片ICH的處理器接口部分,有一個用於兼容老式設備的端口0x92,端口0x92的位1用於控制A20
in al, 0x92
or al, 0000_0010B
out 0x92, al
; 禁止中斷
; 保護模式和實模式下的中斷機制不同,在重新設置保護模式下的中斷環境之前,必須關中斷
cli
; 開啓保護模式
; CR0的第1位(位0)是保護模式允許位(Protection Enabel, PE)
mov eax, cr0
or eax, 1
mov cr0, eax
; 清空流水線、重新加載段選擇器
jmp dword sel_code:protcetmode_beginning
[bits 32]
protcetmode_beginning:
; ....
2.1 構建GDT。
2.2 加載GDT。lgdt指令,將GDT的基地址、界限值載入至GDTR寄存器。
2.3 打開地址線A20。
第21條地址線A20:實模式下,處理器訪問內存的方式是將段寄存器的內容左移4位,再加上偏移地址,以形成20位的物理地址。實模式下,32位處理器的段寄存器的內容僅低20位有效,高20位全部爲0(即,只能使用20根地址線)。故,處理器只能訪問1MB內存。(迴繞)
2.4 禁止中斷。
在設置好保護模式下的中斷環境之前,必須關中斷(指令cli)。保護模式下的中斷機制和實模式不同,原有的中斷向量表IVT不再適用。而且,保護模式下,BIOS中斷也不能再用,因爲它們是實模式下的代碼。
2.5 將CR0的PE位置1,開啓保護模式。
控制實模式/保護模式切換的開關是CR0寄存器。CR0是處理器內部的控制寄存器(Control Register),是32位的寄存器,包含了一系列用於控制處理器操作模式和運行狀態的標誌位。CR0的第1位(位0)是保護模式允許位(Protection Enable, PE),該位置1,則處理器進入保護模式,按保護模式的規則開始運行。
3)加載內核
; FILE: boot/loader.asm
; 截取部分代碼
KERNEL_BIN_BASE_ADDR equ 0x70000 ; 自定義kernel被加載到物理內存位置
KERNEL_START_SECTOR equ 0x09 ; 自定義kernel位於硬盤的扇區號
mov ax, sel_data
mov ds, ax
; 加載kernel,從硬盤讀取到物理內存
; 這裏爲了簡單,選擇了在開啓分頁之前加載
mov eax, KERNEL_START_SECTOR ; kernel.bin在硬盤中的扇區號
mov ebx, KERNEL_BIN_BASE_ADDR ; 從磁盤讀出後,寫入到ebx指定的物理內存地址
mov ecx, 200 ; 讀入的扇區數
call read_hard_disk_0
4)構建頁目錄表和頁表,開啓分頁機制
; FILE: boot/loader.asm
; 截取部分代碼
PAGE_DIR_TABLE_POS equ 0x10_0000 ; 自定義頁目錄表基地址,1MB
mov eax, PAGE_DIR_TABLE_POS ; 頁目錄表PDT基地址
add eax, 0x1000 ; 4KB,此時eax爲第一張頁表的基地址
mov ebx, eax ; 爲.create_pte做準備,ebx爲基址
; 將頁目錄表第0和第0x300即768項都指向第一張頁表, 爲將地址映射爲內核地址做準備
; 0x300項 * 每個頁目錄項對應4MB = 3GB
or eax, PG_US_U | PG_RW_W | PG_P ; 定義該頁目錄項的屬性爲用戶屬性,所有特權級別都可以訪問
mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第1個目錄項, 指向第一張頁表的物理基地址
mov [PAGE_DIR_TABLE_POS + 0x300*4], eax ; 第0x300個目錄項, 也指向第一張頁表
; 一個頁表項佔用4字節, 0xc00表示第768個頁表佔用的目錄項,0xc00以上的目錄項指向內核空間
sub eax, 0x1000 ; 4KB,重新指向自定義的頁目錄PDT基地址
mov [PAGE_DIR_TABLE_POS + 1023*4], eax ; 使最後一個目錄項指向頁目錄表自己的地址
; 騷trips, 用於後面修改頁目錄項和頁表項
; 創建頁表項(PTE)Page Table Entry
; 本項目的mbr、loader、內核都放置在物理內存的低端1MB內
mov ecx, 256 ; 1M低端內存 / 每頁大小4k = 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P ; 屬性爲7,US=1,RW=1,P=1
.create_pte:
mov [ebx+esi*4], edx ; ebx已賦值爲0x10_1000, 即自定義的第一張頁表基地址
add edx, 4096 ; 4KB
inc esi
loop .create_pte
; 頁目錄中創建內核其它頁表的PDE ???
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000 ; 此時eax爲第二張頁表的位置
or eax, PG_US_U | PG_RW_W | PG_P ; 頁目錄項的屬性US,RW和P位都爲1
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254 ; 第769~1022的所有目錄項數量
mov esi, 769
.create_kernel_pde:
mov [ebx+esi*4], eax
inc esi
add eax, 0x1000 ; 頁大小爲4KB
loop .create_kernel_pde
sgdt [gdt_size] ; 存儲到原來gdt的位置
add dword [gdt_base], 0xc000_0000 ; 全局描述符表寄存器GDTR也用的是線性地址
lgdt [gdt_size] ; 將修改後的GDT基地址和界限值加載到GDTR
; 令CR3寄存器指向頁目錄
mov eax, PAGE_DIR_TABLE_POS ; 把頁目錄地址賦給控制寄存器cr3
mov cr3, eax
; 開啓分頁機制
; 從此,段部件產生的地址就不再被看成物理地址,而是要送往頁部件進行變換,以得到真正的物理地址
mov eax, cr0
or eax, 0x8000_0000 ; 打開cr0的pg位(第31位),開啓分頁機制
mov cr0, eax
4.1 物理內存1MB之上:
第1個4KB, 爲頁目錄表PDT
第2個4KB, 爲創建的第一張頁表(第0和第768(0x300)個頁目錄項都指向它)
第769~1022個頁目錄項共指向254個頁表
最後一個頁目錄項(第1023個)指向頁目錄表PDT本身
因此,共256個頁,正好1M。即,物理內存1MB之上的1MB已用於頁目錄表和頁表。
4.2 控制寄存器CR3指向頁目錄表基地址
4.3 將CR0的PG位置1,開啓分頁機制
5)解析kernel的ELF,將ELF文件中的段segment拷貝到各段自己被編譯的虛擬地址處
; FILE: boot/loader.asm
; 截取部分代碼
KERNEL_BIN_BASE_ADDR equ 0x70000; 自定義kernel被加載到物理內存位置
; 遍歷段時,每次增加一個段頭的大小e_phentsize
mov dx, [KERNEL_BIN_BASE_ADDR + 42] ; 偏移42字節處是屬性e_phentsize, 表示program header大小
; 爲了找到程序中所有的段,必須先獲取程序頭表(程序頭program header的數組)
mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; 偏移28字節是e_phoff, 表示第1個program header在文件中的偏移量
; 其實該值是0x34, 不過還是謹慎一點,這裏來讀取實際值
add ebx, KERNEL_BIN_BASE_ADDR ; 加上內核的加載地址,得程序頭表的物理地址
; 程序頭的數量e_phnum,即段的數量,
mov cx, [KERNEL_BIN_BASE_ADDR + 44] ; 偏移44字節是e_phnum, 表示有幾個program header
; 遍歷段
.each_segment:
cmp byte [ebx + 0], 0 ; 若p_type等於0, 說明此program header未使用
je .PTNULL
; 爲函數memcpy壓入參數, 參數是從右往左依次壓入
; 函數原型類似於 memcpy(dst, src, size)
push dword [ebx + 16] ; 壓入memcpy的第3個參數size
; program header中偏移16字節的地方是p_filesz
mov eax, [ebx + 4] ; program header中偏移4字節的位置是p_offset
add eax, KERNEL_BIN_BASE_ADDR ; 加上kernel.bin被加載到的物理地址, eax爲該段的物理內存地址
push eax ; 壓入memcpy的第2個參數src源地址
push dword [ebx + 8] ; 壓入memcpy的第1個參數dst目的地址
; program header中偏移8字節的位置是p_vaddr,這就是目的地址
call mem_cpy ; 調用mem_cpy完成段複製
add esp, 12 ; 清理棧中壓入的三個參數
.PTNULL:
add ebx, edx ; ebx指向下一個program header
; dx爲program header大小, 即e_phentsize
loop .each_segment
將ELF文件中的段segment拷貝到各段自己被編譯的虛擬地址處,將這些段單獨提取到內存中,這就是所謂的內存中的程序映像。分析程序中的每個段segment,如果段類型不是PT_NULL(空程序類型),就將該段拷貝到編譯的地址中
6)跳轉
; FILE: boot/loader.asm
; 截取部分代碼
KERNEL_ENTRY_POINT equ 0xc0001500
mov esp, 0xc009f000 ; ???
jmp KERNEL_ENTRY_POINT
這裏將kernel的入口定義爲 0xc000_1500,對應的在編譯內核kernel.bin時需要指定該地址。
ld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin %.o
=========================================================================================
kernel
// FILE: kernel/main.c
// 截取部分代碼
int main(void)
{
init_all(); // kernel/init.c 初始化所有模塊
cls_screen();
console_put_str("[OS@localhost /]$ ");
// 主線程完成使命後退出
thread_exit(running_thread(), true);
return 0;
}
/* init進程 */
void init(void)
{
unsigned int ret_pid = fork();
if(ret_pid) // 父進程
{
int status, child_pid;
while(1) // init在此處不停地回收過繼給它的子進程
{
child_pid = wait(&status);
printf("i am init, my pid is %d, i recieve a child, it's pid is %d, status is %d\n", child_pid, status);
}
}
else // 子進程
{
my_shell();
}
panic("ERROR: during init, should not be here");
}
其中,init是第一個啓動的程序, pid爲1, 後續的所有進程都是它的孩子。init是所有進程的父進程, 它還要負責回收所有子進程的資源。init是用戶級進程, 因此要調用 process_execute() 來創建進程,這一步是在thread_init()中完成的。
中斷和系統調用
本項目支持的中斷有:時鐘、鍵盤、硬盤、int 0x80(系統調用)。
中斷
|---- FILE: kernel/interrupt.c idt_init()
| | 初始化中斷
| | 構建IDT,這裏IDT中的每一項都指向對應的一段彙編代碼,再由彙編調用C語言中斷處理函數
| | 初始化可編程中斷控制器8259A,放開所需要的中斷
| | 中斷髮生時,會根據IDTR中的IDT基地址+中斷向量*8,跳轉到對應的彙編代碼
| |
| | idt_desc_init() 初始化中斷描述符表 struct gate_desc idt[IDT_DESC_CNT]
| | 中斷描述符中包含了中斷處理程序所在段的段選擇子和段內偏移地址
| | make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i])
| | idt[i]中的每一項都指向對應的一段彙編代碼intr_entry_table[i], 再由彙編調用C語言中斷處理函數idt_table[i]
| | exception_init() 初始化異常名稱, 並註冊通用的中斷處理函數idt_table[i] = general_intr_handler
| | pic_init() 初始化8259A
| | 主片8259A上打開的中斷有: IRQ0的時鐘、IRQ1的鍵盤和級聯從片的IRQ2, 其它全部關閉
| | 從片8259A上打開IRQ14的硬盤
| | asm volatile("lidt %0" : : "m" (idt_operand))
| | 指令lidt把IDT的界限值、基地址加載到IDTR寄存器
| |
| |---- FILE: kernel/core_interrupt.asm
| | | 模板intr_%1_entry重複展開並構成intr_entry_table[]
| |
| |---- FILE: lib/io.h
| | | 內聯彙編實現的讀寫端口函數
| | | 凡是包含io.h的文件,都會獲得一份io.h中所有函數的拷貝
| | | inline是建議處理器將函數編譯爲內嵌方式,即在該函數調用處原封不動地展
| |
| |---- 系統調用
| | | make_idt_desc(&idt[0x80], IDT_DESC_ATTR_DPL3, syscall_handler)
| | | 系統調用對應爲0x80號中斷, 中斷處理程序爲彙編syscall_handler, 再調用中斷處理函數syscall_table[]
| | |
| | |---- FILE: kernel/core_interrupt.asm syscall_handler 系統調用統一入口
| | |---- FILE: user/syscall-init.c syscall_table[] 每個系統調用對應的中斷處理程序
| | |---- FILE: lib/syscall.h SYSCALL_NR 系統調用號
| | |---- FILE: lib/syscall.c 每個系統調用對外(用戶)的接口
內存管理
這裏先從指定位置處讀取LOADER寫入的物理內存大小。本項目中,物理內存的配置爲32M(bochs配置文件bochsrc.cfg中"megs: 32"),減去低端的1MB、減去LOADER開啓分頁機制時創建PDT和PT佔用的1MB(緊鄰低端1MB之上),還有30MB,內核和用戶內存池各佔15M。所以,內核物理內存池的起始地址爲 0x20_0000(2MB)。
4GB虛擬地址空間中,高1GB爲內核空間,其中1GB之上的1MB虛擬空間已在LOADER階段映射到物理內存的低端1MB。所以,內核虛擬地址池的起始地址爲0xc010_0000(1GB+1MB)。
以頁(4KB)爲單位的內存管理,採用bitmap(位圖)技術。本項目中,自定義內核物理內存的bitmap存放於0xc009_a000,自定義內核主線程棧頂爲0xc009_f000、內核主線程PCB爲0xc009_a000。所以,本系統最大支持4個頁框的位圖(一個頁框大小的位圖可表示128M內存,4個頁框即512M),用於內核/用戶物理內存池bitmap、內核虛擬地址池bitmap。
// FILE: kernel/memory.c
// 截取部分代碼
/* 內存池結構,用於管理內存池中的所有物理內存 */
struct pool{
struct lock lock; // 申請內存時互斥, 避免公共資源的競爭
struct bitmap pool_bitmap; // 內存池用到的位圖結構,用於管理物理內存
unsigned int phy_addr_begin; // 內存池所管理物理內存的起始地址
unsigned int pool_size; // 內存池字節容量,本物理內存池的內存容量
};
/* 虛擬地址池結構 */
struct virtual_addr{
struct bitmap vaddr_bitmap; // 虛擬地址用到位圖結構
unsigned int vaddr_begin; // 虛擬地址起始值
};
/* 位圖 */
struct bitmap{
unsigned int bitmap_bytes_len;
unsigned char *bits; // 位圖所在內存的起始地址
};
struct pool kernel_pool, user_pool; // 內核內存池和用戶內存池
struct virtual_addr kernel_vaddr; // 用於給內核分配虛擬地址
/* 初始化內存池 */
static void mem_pool_init(unsigned int mem_size)
{
// 僞代碼
/* 初始化內核物理內存池、用戶物理內存池 */
kernel_pool.phy_addr_begin = 2MB; // 起始地址
user_pool.phy_addr_begin = 2MB+15MB;
kernel_pool.pool_size = 15MB; // 內存池大小
user_pool.pool_size = 15MB;
kernel_pool.pool_bitmap.bitmap_bytes_len = 15MB/4KB/8bits; // 位圖大小
user_pool.pool_bitmap.bitmap_bytes_len = 15MB/4KB/8bits;
kernel_pool.pool_bitmap.bits = (void *)0xc009_a000; // 指定位圖起始地址
user_pool.pool_bitmap.bits = (void *)(0xc009_a000 + kbm_length);
bitmap_init(&kernel_pool.pool_bitmap); // 初始化位圖
bitmap_init(&user_pool.pool_bitmap);
lock_init(&kernel_pool.lock); // 初始化鎖
lock_init(&user_pool.lock);
/* 初始化內核虛擬地址池 */
kernel_vaddr.vaddr_bitmap.bitmap_bytes_len = kbm_length; // 與內核物理內存池大小一致
// 這裏將其安排在緊挨着內核內存池和用戶內存池所用的位圖之後
kernel_vaddr.vaddr_bitmap.bits = (void *)(0xc009_a000 + kbm_length + ubm_length);
kernel_vaddr.vaddr_begin = 0xc010 0000; // 內核虛擬地址池的起始地址, 即3G+1M
bitmap_init(&kernel_vaddr.vaddr_bitmap); // 初始化位圖
}
基於bitmap,實現了以頁爲單位的內存管理。 虛擬地址是連續的,但物理地址可能連續,也可能不連續。一次性申請count個虛擬頁之後,再依次爲每一個虛擬頁申請物理頁,並在頁表中依次添加映射關聯。
在以頁(4KB)爲單位的內存管理基礎上,實現小內存塊的管理,可滿足任意內存大小的分配與釋放(malloc/free)。這裏採用arena模型。
/* 內存塊描述符信息 */
struct mem_block_desc{
unsigned int block_size; // 內存塊大小
unsigned int blocks_per_arena; // 每個arena可容納此mem_blcok的數量
struct list free_list; // 空閒內存塊mem_block鏈表
};
/* 內存倉庫arena 元信息 */
struct arena{
struct mem_block_desc *desc; // 此arena關聯的mem_block_desc
unsigned int count; // large爲true時, count表示頁框數; 否則, 表示空間mem_block數量
bool large; // 內存分配大於1024字節時爲true
};
/* 內存塊 */
struct mem_block{
struct list_elem free_elem;
};
// 內核內存塊描述符數組
// 本系統支持7種規格的內存塊: 16 32 64 128 256 512 1024字節
struct mem_block_desc k_block_descs[7];
內存管理系統
|---- FILE: kernel/memory.c mem_init()
| | 初始化內存管理系統
| | mem_pool_init() 初始化內存池: 內核虛擬地址池、內核/用戶物理內存池
| | 虛擬地址池: 虛擬地址bitmap、虛擬地址池起始地址
| | 物理內存池:物理內存bitmap、物理內存起始地址、物理內存池大小
| | block_desc_init(k_block_descs) 初始化內核內存塊描述符數組struct mem_block_desc k_block_descs[7]
| | 7種規格: 16 32 64 128 256 512 1024字節
| | 用戶進程也有自己的內存塊描述符數組, 定義在PCB中
| |
| |---- FILE: lib/bitmap.c bitmap_init() bitmap_scan() bitmap_set()
| | bitmap的基本操作
|
|---- FILE: kernel/memory.c malloc_page()
| | 頁爲單位的內存分配(基於bitmap技術)
| | 虛擬地址池中一次性申請count個虛擬頁 vaddr_get()
| | 依次爲每個虛擬頁申請物理頁, 並在頁表中做映射
| | palloc() 在物理內存池中分配一個物理頁
| | page_table_add() 頁表中添加虛擬地址與物理地址的映射
| | 二級頁表映射
| | pde_ptr(vaddr) 若頁目錄項不存在, 則先從內核空間申請一個物理頁, 再將物理地址及屬性寫入PDE
| | pte_ptr(vaddr) 在虛擬地址對應的頁表項中PTE寫入物理地址及其屬性
| |
| FILE: kernel/memory.c mfree_page()
| | 頁爲單位的內存釋放
| | addr_v2p(vaddr) 獲取虛擬地址對應的物理地址, 判斷是內核/用戶物理內存池
| | pfree(pg_phy_addr) page_table_pte_remove() 物理頁挨個歸還給物理內存池, 並清除虛擬地址所在的PTE
| | vaddr_remove() 一次性將連續的cout個虛擬頁地址歸還給虛擬地址池
| |
| |---- FILE: lib/bitmap.c bitmap_scan() bitmap_set()
|
|
|---- FILE: kernel/memory.c sys_malloc()
| | 任意內存大小的分配(基於arena模型)
| | 判斷是內核線程還是用戶進程, 再從塊描述符數組struct mem_block_desc中匹配合適的規格
| | 若該規格的free_list爲空, 申請一頁內存作爲arena, 再將arena拆分成該規格的內存塊, 並添加到free_list
| | 內存分配: list_pop(&(descs[desc_index].free_list)
| |
| FILE: kernel/memory.c sys_free()
| | 任意內存大小的釋放
| | 先將內存塊回收到free_list:
| | struct mem_block *bk = vaddr
| | struct arena *ar = block2arena(bk)
| | list_append(&ar->desc->free_list, &bk->free_elem)
| | 再判斷此arena中的內存塊是否都空閒, 若是則釋放arena
| | if(++ar->count == ar->desc->blocks_per_arena)
| | 將arena中所有的內存塊從free_list中去掉, 釋放arena(4KB頁)
| |
| |---- FILE: lib/list.c 鏈表 list_empty() list_append() list_remove()
| |---- FILE: thread/sync.c 鎖 lock_acquire() lock_release()
| |---- FILE: kernel/memory.c malloc_page() mfree_page() arena2block() block2arena()
物理地址/線性地址/虛擬地址/邏輯地址:
1)實模式下,"段基址+段內偏移地址"經過段部件的處理,直接輸出的就是物理地址,CPU可以直接用此地址訪問內存。
2)保護模式下,"段基址+段內偏移地址"爲線性地址。但此處的段基址不再是真正的地址,而是一個選擇子,本質上是個索引,類似於數組下標,通過這個索引便能在GDT中找到相應的段描述符。段描述符記錄了該段的起始、大小等信息,這樣便得到了段機制。若沒有開啓地址分頁功能,此線性地址就被當作物理地址來用,可直接訪問內存。
3)保護模式+分頁機制,若開啓了分頁功能,線性地址則稱爲虛擬地址(虛擬地址、線性地址在分頁機制下都是一回事)。虛擬地址要經過CPU頁部件轉換成具體的物理地址,這樣CPU才能將其送上地址總線取訪問內存。
邏輯地址,無論是在實模式或保護模式下,段內偏移地址又稱爲有效地址,也稱爲邏輯地址,這是程序員可見的地址。最終的地址是由段基址和段內偏移地址組合而成。實模式下,段基址在對應的段寄存器中(cs ds es fs gs);保護模式下,段基址在段選擇子寄存器指向的段描述符中。所以,只要給出段內偏移地址就行了,再加上對應的段基址即可。
訪問外部硬件有2個方式:
1)將某個外設的內存映射到一定範圍的地址空間中,CPU通過地址總線訪問該內存區域時會落到外設的內存中,這種映射讓CPU訪問外設的內存就如同訪問主板上的物理內存一樣。比如顯卡,顯卡是顯示器的適配器,CPU不直接和顯示器交互,只和顯卡通信。顯卡上有片內存叫顯存,被映射到主機物理內存上的低端1MB的0xB 8000 ~ 0xB FFFF。CPU訪問這片內存就是訪問顯存,往這片內存上寫字節便是往屏幕上打印內容。
2)外設是通過IO接口與CPU通信的,CPU訪問外設,就是訪問IO接口,由IO接口將信息傳遞給另一端的外設。CPU從來不知道有這些設備的存在,它只知道自己操作的IO接口。
將數據和代碼分開的好處:
1)可以賦予不同的屬性,使程序更安全。如,數據,只讀/只寫/可讀可寫;代碼,只讀。
2)提高CPU內部緩存的命中率,使程序運行得更快。局部性原理,CPU內部有針對數據和針對指令的兩種緩存機制。
3)節省內存。當一個程序的多個副本同時運行(比如同時執行多個ls命令),可以把只讀的代碼段共享,沒必要在內存中同時存在多個相同的代碼段。