GITHUB: https://github.com/trb331617/miniOS
運行效果:
實驗環境:
VMware Workstation 15 Pro; CentOS-7-x86_64-Minimal-1908;
bochs-2.6.8; gcc-4.8.5;
References:
《操作系統真象還原》,鄭鋼
《x86彙編語言:從實模式到保護模式》,李忠
《彙編語言(第3版)》,王爽
=========================================================================================
Linux中90%以上的代碼都是用在資源管理、調度策略、算法及數據結構等方面。操作系統受制於硬件的支持, 很大程度上它的能力取決於硬件的能力,很多操作都是硬件自動完成的。比如,
1) 處理器進入0特權級時, 會自動在任務狀態段TSS中獲得0特權級的棧地址;
2) 中斷髮生後, 處理器由低特權級進入高特權級, 它會自動把ss3 esp3 eflags cs eip依次壓入棧中(轉移後的高特權級棧)。
3) 所以,執行iret指令從高特權級返回低特權級時,處理器可以從當前高特權級棧中獲取低特權級的棧段選擇子及偏移量。
……
因此,要想全面理解操作系統,不僅需要了解上層軟件的算法、原理、實現, 還要了解很多硬件底層的內容。
本項目實現的mini操作系統,包含:
1)內核線程、用戶進程、fork和execv、任務調度;
2)中斷(時鐘, 鍵盤, 硬盤, 系統調用等)、內存管理、文件系統、shell、管道;
3)基於二元信號量的鎖、環形隊列。
mbr, 512Bytes; loader, 1.4KB; kernel, 87KB, 8406lines C.
=========================================================================================
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, 並跳轉。
(本項目中, mbr所在的位置, 將會在loader階段解析kernel ELF時覆蓋)
; 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 ; 使最後一個目錄項指向頁目錄表自己的地址
; 騷trik, 用於後面修改頁目錄項和頁表項, 用於查找虛擬地址對應的物理地址
; 創建頁表項(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 ; 自定義內核主線程PCB中的棧頂
jmp KERNEL_ENTRY_POINT
這裏將kernel的入口定義爲 0xc000_1500,對應的在編譯內核kernel.bin時需要指定該地址。
ld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin %.o
=========================================================================================
kernel
main
init是內核啓動的第一個程序, pid爲1, 後續的所有進程都是它的孩子。init是所有進程的父進程, 它還要負責回收所有子進程的資源。init是用戶級進程, 因此要調用 process_execute() 來創建進程,這一步是在init_all()中的thread_init()中完成的。
// 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");
}
內核的main_thread完成系統的初始化工作,然後thread_exit。
// FILE: kernel/init.c
// 截取部分代碼
/* 初始化所有模塊 */
void init_all()
{
idt_init(); // 初始化中斷
mem_init(); // 初始化內存管理系統
thread_init(); // 初始化main_thread線程, 創建init進程、idle線程
timer_init(); // 初始化PIT, 可編程定時計時器Programmable Interval Timer
console_init(); // 初始化控制檯
keyboard_init(); // 初始化鍵盤
tss_init(); // 初始化TSS(任務狀態段)並安裝到GDT, 同時安裝DPL爲3的代碼段和數據段
syscall_init(); // 初始化系統調用
intr_enable(); // ide_init 需要打開中斷
ide_init(); // 初始化硬盤
filesys_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 系統調用統一入口
| | | | 從內核棧中獲取cpu自動壓入的用戶棧指針esp
| | | | 從用戶棧中讀取系統調用參數, 再壓入內核棧
| | | | call子功能號對應的系統調用實現
| | | | intr_exit從中斷返回
| | |---- 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()
二級頁表映射
|---- trick: 頁目錄表的基地址賦值給CR3和最後一個頁目錄項
|
|---- FILE: kernel/memory.c addr_v2p()
| | 虛擬地址轉換爲物理地址
| | 頁表項所在的物理地址 pte = pte_ptr(vaddr)
| | 頁表項的值去掉屬性+虛擬地址的偏移部分 (*pte & 0xfffff000) + (vaddr & 0x00000fff)
| |
| |---- pte_ptr()
| | | 得到虛擬地址對應頁表項所在的物理地址
| | | (0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) * 4)
| |
| |---- PDE_IDX()
| | | 得到虛擬地址所在頁表的索引號
| | | ((addr & 0x003ff000) >> 12)
// 由結構體成員得到起始地址
#define offset(struct_type, member) (int)(&((struct_type *)0)->member)
#define elem2entry(struct_type, struct_member_name, elem_ptr) \
(struct_type *)((int)elem_ptr - offset(struct_type, struct_member_name))
內核線程/用戶進程
內核主線程/idle線程/init進程
內核線程/用戶進程
|---- FILE: thread/thread.c thread_init()
| | 初始化線程環境
| | list_init() 初始化就緒/全部隊列
| | pid_pool_init() 初始化pid池, 指定起始pid爲1 (基於bitmap和lock)
| |
| | process_execute(init, "init") 創建第一個用戶進程init, 其pid爲1
| | make_main_thread() 將當前內核main函數創建爲線程
| | idle_thread = thread_start("idle", 10, idle, NULL) 創建idle線程
| |
| | 至此, 參與調度的有: init進程、main線程、idle線程
| |
| |---- FILE: user/process.c process_execute()
| | | 創建用戶進程
| | | struct task_struct *thread = get_kernel_pages(1) 申請1頁內核內存作爲用戶進程PCB
| | | init_thread() 在PCB中初始化線程基本信息
| | | 申請pid allocate_pid()
| | | {bitmap_scan(); bitmap_set(); return (index + pid_pool.pid_start);}
| | | 進程名、狀態READY、內核態時的棧頂(PCB頂部)、優先級、嘀嗒時間數
| | | 初始化文件描述符數組pthread->fd_table[8], 標準輸入/輸出/錯誤012, 其餘爲-1
| | | 根目錄作爲默認工作路徑 pthread->current_work_dir_inode_id = 0
| | | 父進程pid初始爲-1
| | | 頁目錄表pgdir初始爲NULL, 在緊接着的create_page_dir()創建頁目表時賦值
| | | create_user_vaddr_bitmap() 創建用戶虛擬內存空間的bitmap
| | | 指定用戶進程虛擬地址起始值; 申請1頁內核內存
| | |
| | | thread_create(thread, start_process, filename)
| | | 初始化PCB中的thread_stack。當處理器進入kernel_thread函數體時,
| | | 棧頂爲返回地址、棧頂+4爲參數function、棧頂+8爲參數func_arg
| | | kthread_stack->eip = kernel_thread 函數kernel_thread
| | | kthread_stack->function = start_process 函數start_process
| | | kthread_stack->func_arg = filename 待創建的進程
| | |
| | | thread->pgdir = create_page_dir() 創建頁目錄表
| | | 在內核空間申請一頁內存作爲用戶進程的頁目錄表 get_kernel_pages(1)
| | | 從內核頁目錄表中拷貝內核空間的頁目錄項到用戶進程的頁目錄中 memcpy()
| | | 頁目錄表物理基地址寫入頁目錄表最後一項
| | |
| | | block_desc_init(thread->u_block_desc) 初始化內存規格信息, 爲malloc/free做準備
| | | list_append() 添加到就緒隊列和全部隊列
| |
| |---- FILE: thread/thread.c make_main_thread()
| | | 將kernel中main函數完善爲主線程
| | | main線程早已運行, 在LOADER階段已預留並指定了PCB "mov esp, 0xc009_f000"
| | | 即, PCB基址爲0xc009_e000
| | | main_thread = running_thread();
| | | init_thread(main_thread, "main", 31);
| | | list_append() 添加到全部隊列
| |
| |---- FILE: thread/thread.c thread_start()
| | | 創建內核線程
| | | struct task_struct *thread = get_kernel_pages(1) 申請1頁內核內存作爲內核線程PCB
| | | init_thread() 在PCB中初始化線程基本信息
| | | 初始化PCB底部的線程信息struct task_struct
| | | 申請pid allocate_pid()
| | | thread_create(thread, function, func_arg)
| | | 初始化PCB中的thread_stack
| | | 不同於創建進程時的參數function爲start_process, 這裏直接爲所要創建的線程,
| | | 即switch_to()任務切換後, 將直接執行對應的線程函數
| | | kthread_stack->eip = kernel_thread 函數kernel_thread
| | | kthread_stack->function = function 線程對應的函數
| | | kthread_stack->func_arg = func_arg 線程參數
| | |
| | | list_append() 添加到就緒隊列和全部隊列
|
|---- FILE: thread/thread.c idle()
| | 系統空閒時運行的線程(block/unblock)
| | while(1){thread_block(TASK_BLOCKED); asm volatile("sti; hlt" : : : "memory");}
| | idle線程在創建時會被加入到就緒隊列, 因此會執行一次, 然後阻塞;
| | 當就緒隊列爲空時, schedule會將idle解除阻塞, 也就是喚醒
| |
| |---- thread_block()
| | | 當前線程將自己阻塞
| | | 修改線程狀態爲阻塞、觸發調度, 切換線程執行
| | | schedule() 由當前阻塞線程主動觸發的任務調度
| | | 由於idle線程觸發調度後沒有被加入就緒隊列, 所以將得不到執行, 除非被喚醒
| |
| |---- thread_unblock()
| | | 將指定的線程解阻塞
| | | 添加到就緒隊列頭部, 修改狀態爲READY
| | | list_push(&thread_ready_list, &pthread->general_tag)
| | | pthread->status = TASK_READY
|
|---- FILE: kernel/main.c main()
| | 內核主線程
| | init_all()
| | thread_exit(running_thread(), true);
| | 主線程完成使命後退出 return 0;
|
|---- FILE: kernel/main.c init()
| | init進程
| | 第一個用戶進程, pid爲1
| | init是所有進程的父進程, 它還要負責回收過繼給它的子進程
| | if(fork()) // 父進程
| | { while(1){wait(&status);} }
| | else // 子進程
| | { my_shell(); }
fork/wait/exit
fork/wait/exit
|---- FILE: lib/syscall.c fork()
|---- FILE: user/fork.c sys_fork()
| | 創建子進程
| | 1) 申請1頁內核內存作爲子進程PCB get_kernel_pages(1)
| | 2) 複製父進程的資源 copy_process(child, parent)
| | 3) 添加到就緒隊列和全部隊列 list_append()
| | 4) 父進程返回子進程的pid return child_thread->pid
| |
| |---- FILE: user/fork.c copy_process()
| | 複製父進程的資源
| | 1) 複製父進程的PCB、虛擬地址位圖到子進程, 申請進程pid, 修改子進程的進程信息
| | copy_pcb_vaddr_bitmap_stack0()
| | 2) 爲子進程創建頁表, 並複製內核1G空間對應的頁目錄項 create_page_dir()
| | 3) 複製父進程進程體(代碼和數據資源)給子進程 copy_body_stack3()
| | 逐字節再逐位查看位圖, 若有數據則先拷貝到內核空間
| | 切換爲子進程的頁表, 再將數據從內核空間複製到子進程的用戶空間
| | 恢復父進程的頁表
| | 4) 構建子進程thread_stack和修改返回值pid build_child_stack()
| | intr_0_stack->eax = 0 // 根據abi約定, eax爲函數返回值, fork爲子進程返回0
| | *(intr_0_stack - 1) = intr_exit // switch_to的返回地址設置爲intr_exit, 直接從中斷返回
| | // 子進程被調度時, 直接從中斷返回, 即實現了從fork之後的代碼處繼續執行
| | 5) 更新文件inode的打開數 update_inode_open_counts()
|
|---- FILE: lib/syscall.c wait(&status)
|---- FILE: user/wait_exit.c sys_wait()
| | 等待子進程調用exit再回收子進程
| | while(1){
| | // 子進程狀態爲HANGING, 即已調用exit, 則回收子進程
| | child = list_traversal(&thread_all_list, find_hanging_child, parent_thread->pid)
| | thread_exit(child_thread, false); // 傳入false, 使thread_exit執行完後回到此處
| | return child_pid
| | // 否則, 說明子進程仍在運行, 則將自己掛起
| | child_elem = list_traversal(&thread_all_list, find_child, parent_thread->pid);
| | thread_block(TASK_WAITING);
| |
| |---- FILE: thread/thread.c thread_exit()
| | 回收線程/子進程的PCB和頁目錄表, 並從調度隊列中刪除
| | mfree_page(PF_KERNEL, thread->pgdir, 1); // 回收頁目錄表
| | mfree_page(PF_KERNEL, thread, 1); // 回收PCB
| | list_remove() // 從調度隊列中刪除
| | release_pid() // 釋放pid
|
|---- FILE: lib/syscall.c exit()
|---- FILE: user/wait_exit.c sys_exit()
| | 子進程退出
| | child_thread->exit_status = status // 將status存入自己的pcb
| | // 將進程child的所有子進程都過繼給init
| | list_traversal(&thread_all_list, init_adopt_a_child, child_thread->pid);
| | release_prog_resourece(child_thread) // 回收進程child的資源
| | // 如果父進程正在waiting, 則先喚醒父進程
| | if(parent_thread->status == TASK_WAITING) thread_unblock(parent_thread);
| | // 將自己掛起, 等待父進程獲取其status, 並回收其pcb
| | thread_block(TASK_HANGING);
| |
| |---- FILE: user/wait_exit.c release_prog_resourece()
| | 釋放用戶進程資源
| | 1) 遍歷頁目錄表和頁表將用戶空間部分已分配的用戶物理頁在位圖中清0
| | free_a_phy_page(page_phy_addr)
| | 2) 將用戶虛擬地址池bitmap本身佔用的內核物理頁釋放
| | mfree_page(PF_KERNEL, bitmap, bitmap_page_count);
| | 3) 關閉進程打開的文件
| | sys_close(fd_index)
任務調度
1) 基於時鐘中斷的任務調度, 時鐘的中斷處理程序 intr_timer_handler()
處理器進入0特權級時, 會自動在任務狀態段TSS中獲得0特權級的棧地址, 即current線程PCB頂部的struct intr_stack
中斷的入口intr_%1_entry會執行一系列壓棧操作(FILE: kernel/core_interrupt.asm),
(若是從低特權級進入高特權級, 還會自動壓棧: ss esp eflags cs eip err)
2) (此時是current的PCB) 函數schedule()中, 若next爲進程則修改TSS的esp0(next被中斷時使用, 這裏還是current的), 然後
調用 switch_to(current_thread, next), 將自動在PCB頂部繼續壓棧(可參考struct thread_stack):
next current_thread eip/retaddr(這裏的eip指向的是函數schedule()中switch_to的下一條指令代碼)
3) switch_to()
1. (此時是current的PCB) 繼續壓棧: esi edi ebx ebp
2. (爲current保存esp到PCB底部) 從[esp+5*4]處得到棧中參數current_thread(即PCB底部), 再mov [eax], esp
3. (切換esp爲next的esp) 從[esp+6*4]處得到棧中參數next(即PCB底部, PCB底部保存的是esp值), 再mov esp, [eax]
4. (此時是next的PCB) 出棧: ebp ebx edi esi
5. (利用ret自動從棧中彈出給eip的特性實現執行流的切換) ret
若該任務是第一次執行, 則此時eip爲kernel_thread()函數(這是在thread_create()函數中初始化的)
執行kernel_thread()時會將棧中的2個值當作其參數function和arg, 執行function(arg);
(棧中的function和arg也是在thread_create()中初始化的)若爲線程, function爲直接線程函數;
若爲進程, function爲start_process(), arg爲進程程序
若該任務並非第一次執行, 則此時eip指向的是函數schedule()中switch_to的下一條指令代碼(類似於這裏的current)
而swit_to是schedule最後一句代碼, 因此執行流程馬上回到schedule的調用者intr_timer_handler中
schedule也是intr_timer_handler中最後一句代碼, 因此會回到core_interrupt.asm中的jmp intr_exit,
從而恢復任務的全部寄存器映像, 之後通過iretd指令退出中斷, 任務被完全徹底恢復
基於時鐘中斷的任務調度
|---- FILE: device/timer.c timer_init()
| | frequency_set()初始化可編程定時計時器8253
| | 使用8253來給IRQ0引腳上的時鐘中斷信號“提速”,使其發出的中斷信號頻率快一些。
| | 默認的頻率是18.206Hz,即一秒內大約發出18 次中斷信號。
| | 通過對8253編程,使時鐘一秒內發100次中斷信號,即中斷信號頻率爲100Hz.
| |
| | register_handler(0x20, intr_timer_handler) FILE: kernel/interrupt.c
| | 註冊安裝中斷處理程序 idt_table[vector_id] = function
| | 時鐘的中斷向量號0x20
| |
| |---- FILE: device/timer.c intr_timer_handler()
| | 時鐘的中斷處理程序
| | running_thread()通過esp得到PCB基地址 asm("mov %%esp, %0" : "=g"(esp))
| | if(current_thread->ticks == 0)
| | schedule(); // 若當前任務時間片用完,則調度
| | else
| | current_thread->ticks--; // 將當前任務的時間片-1
| |
| |---- FILE: thread/thread.c schedule()
| | 任務調度 (由時鐘中斷觸發)
| | *** 調度 ****************
| | 1) 若狀態爲RUNNING說明時間片到了, 則添加到就緒隊列尾並重置ticks、狀態改爲READY
| | 2) 就緒隊列
| | 若就緒隊列爲空, 則喚醒idle_thread線程 thread_unblock(idle_thread)
| |
| | 正常則彈出第一個線程, 由結構體成員得到首地址(PCB基地址), 狀態改爲RUNNING
| | 3) process_activate(next) FILE: user/process.c
| | 更新CR3切換頁目錄表, 進程還需要修改TSS的esp0
| |
| | 4) switch_to(current_thread, next) 切換任務
| |
| |---- FILE: user/process.c process_activate(next)
| | | 更新CR3切換頁目錄表, 用戶進程還需要修改TSS的esp0
| | | page_dir_activate() 更新CR3
| | | 內核線程頁目錄基地址爲0x10_0000, 用戶進程爲addr_v2p(pthread->pgdir)
| | | asm volatile("movl %0, %%cr3" : : "r"(pagedir_phy_addr) : "memory")
| | | update_tss_esp(pthread) 若是用戶進程, 則需要更新TSS中的esp0
| | | tss.esp0 = (unsigned int)pthread + PAGE_SIZE
| | | 模仿Linux: 一個CPU上的所有任務共享同一個TSS,之後不斷修改同一個TSS的內容
| | | TSS中esp0字段的值爲pthread的0級棧, 用戶進程由用戶態進入內核態時所用的棧
| | | 處理器會自動到TSS中獲取esp0作爲用戶進程在內核態的棧地址
| |
| |---- FILE: thread/switch.asm switch_to(current_thread, next);
| | 切換棧、切換執行流eip
| | *********************************************
| | 此處的理解需結合線程棧信息struct thread_stack和函數thread_create()
| | *********************************************
| | 保存current線程的寄存器, 將next線程的寄存器裝載到處理器
| | 傳入的2個參數自動壓入了current棧中, 這2個參數爲2線程PCB基地址
| | PCB底部爲線程/進程信息struct task_struct, 第一個成員爲棧頂地址
| | 切換棧: 僞代碼 mov [current], esp; mov esp, [next];
| | 切換執行線程: ret。這裏利用了ret的特性, 自動從棧彈出給eip
| | 棧中存放eip處, 已由函數thread_create賦值爲kernel_thread()
| |
| |---- FILE: thread/thread.c kernel_thread(function, func_arg)
| | 執行創建線程時指定的函數/執行進程啓動函數
| | 這裏2個參數的值已由函數thread_create賦值在棧中
| | intr_enable()開中斷, 避免時鐘中斷被屏蔽而無法調度其它線程
| | function(func_arg)
| | 若爲線程,則執行新線程對應的函數
| | 若爲進程,則執行start_process(filename)
| |
| |---- FILE: user/process.c start_process(filename)
| | 開啓用戶進程
| | 假裝從中斷返回, 由0特權級進入用戶態3特權級
| | 1) 假裝。構造用戶進程的中斷棧信息struct intr_stack(位於PCB頂部)
| | edi esi ebp esp_dummy; ebx edx ecx eax; gs ds es fs;
| | (待執行的用戶進程)eip cs eflags; (用戶空間的3特權級棧)esp ss
| | 2) 從中斷返回。jmp intr_exit
| |
| |---- FILE: kernel/core_interrupt.asm intr_exit
| | 從中斷返回 iretd
文件系統(待補充)
<------ 待補充 ------>
shell
shell, 由init()進程fork()的子進程
|---- FILE: shell/shell.c my_shell()
| | while(1){
| | print_prompt()
| | readline()
| | if(pipe_symbol){ 包含管道符, 則重定向stdin stdout, 並依次執行cmd_execute() }
| | else { 無管道, 則直接執行cmd_execute(argc, argv) }
| | }
| |
| |---- FILE: shell/shell.c cmd_execute()
| | 執行命令
| | 內部命令: ls cd pwd ps clear mkdir rmdir rm help,
| | 則直接系統調用
| | 外部命令: cat
| | if(fork()) // 父進程
| | child_pid = wait(&status)
| | else // 子進程
| | execv(argv[0], argv)
| |
| |---- FILE: lib/syscall.c execv()
| |---- FILE: user/exec.c sys_execv()
| | 加載新程序替換當前進程並執行
| | 加載 entry_point = load(path)
| | 修改進程名 memcpy(curr->name, path, TASK_NAME_LEN)
| | 構建內核棧
| | ebx = argv ecx = argc
| | eip = entry_point esp = 0xc000_0000
| | 假裝從中斷返回, 執行新程序
| | exec不同於fork, 爲使新程序更快被執行, 直接從中斷返回
| | asm volatile("movl %0, %%esp; jmp intr_exit" : : "g" (intr_0_stack) : "memory")
| |
| |---- FILE: user/exec.c load()
| | | 從文件系統加載用戶程序, 並返回程序入口elf_header.e_entry
| | | 解析ELF文件
| |
| |---- FILE: kernel/core_interrupt.asm intr_exit
| | 從中斷返回 iretd
user
簡易版C運行庫
C運行庫, C RunTime Library, CRT. CRT主要功能是初始化運行環境,然後調用用戶進程的main(),最後調用exit()回收用戶進程的資源。
CRT代碼纔是用戶程序的第一部分,用戶進程的main()實質上是被夾在CRT中執行的。
使用ar命令將start.o和所需要的庫文件打包成靜態庫文件simple_crt.a,即本項目中的簡陋版C運行庫。
start.asm:call main; call exit
; FILE: command/start.asm
[bits 32]
extern main
extern exit
SECTION .text
; 用戶程序真正的第一個函數, 是程序真正入口
GLOBAL _start ; _start 鏈接器默認的入口符號, 也可以 ld -e 指定入口符號
_start:
; 下面這2個要和execv()和load()之後指定的寄存器一致
push ebx ; 壓入 argv
push ecx ; 壓入 argc
call main
; ABI規定, 函數返回值是在eax寄存器中
; 將main的返回值通過棧傳給exit, gcc用eax存儲返回值, ABI規定
push eax ; 把main的返回值eax壓棧
; 爲下面調用exit系統調用壓入的參數, 相當於exit(eax)
call exit ; exit 將不會返回
編譯鏈接
# ar 命令將string.o syscall.o stdio.o assert.o start.o打包成靜態庫文件simple_crt.a
ar rcs simple_crt.a $OBJS start.o
# 編譯用戶程序
gcc $CFLAGS $LIBS -o $BIN".o" $BIN".c"
# 鏈接
ld -m elf_i386 $BIN".o" simple_crt.a -o $BIN # 默認 -e _start
lib
鎖
鎖
| struct lock pid_lock; // 申請pid FILE: thread/thread.c
| struct lock console_lock; // 控制檯鎖 FILE: device/console.c console_init()
|
|---- FILE: thread/sync.c lock_init(&console_lock)
| | 初始化鎖
| | 基於二元信號量實現的鎖
| | struct semaphore{
| | unsigned char value; struct list waiters; };
| | 信號量初值, 此信號量上阻塞的所有線程
| | struct lock{
| | struct task_struct *holder; struct semaphore sema; unsigned int holder_repeat_num; };
| | 鎖的持有者, 信號量, 鎖的持有者重複申請鎖的次數
|
|---- FILE: thread/sync.c lock_acquire()
| | 獲取鎖
| | sema_down(&lock->semaphore)信號量P操作
| | while(sema->value == 0) // value爲0表明已經被別人持有
| | list_append()當前線程把自己加入該鎖的等待隊列
| | thread_block()當前線程阻塞自己,並觸發調度,切換線程
| | ************************************* 調度 *********
| | lock->holder = running_thread()
| |
| |---- FILE: thread/thread.c thread_block()
| | 當前線程將自己阻塞
| | 修改線程狀態爲阻塞、觸發調度, 切換線程執行
| |
| |---- FILE: thread/thread.c schedule()
| | 任務調度 (由當前阻塞線程主動觸發)
|
|---- FILE: thread/sync.c lock_release()
| 釋放鎖
| lock->holder = NULL
| sema_up(&lock->semaphore)信號量V操作
| list_pop(&sema->waiters)從等待隊列中取出一個線程
| thread_unblock()喚醒該阻塞線程: 將阻塞線程加入就緒隊列,並修改狀態爲READY
| sema->value++
|
|---- FILE: thread/thread.c thread_unblock()
| 喚醒阻塞線程
| 將阻塞線程加入就緒隊列,並修改狀態爲READY
環形隊列
環形隊列
| struct ioqueue keyboard_buf; // 鍵盤
| struct ioqueue * xxx = xxx; // 管道
|
|---- FILE: device/ioqueue.c ioqueue_init(&keyboard_buf)
| | 初始化環形隊列
| | 結合鎖機制、生產者消費者模型
| | struct ioqueue{
| | struct lock lock; // 鎖
| | struct task_struct *producer, *consumer; // 睡眠的生產者/消費者
| | char buf[buffersize]; // 緩衝區
| | signed int head, tail; }; // 隊首寫入, 隊尾讀出
|
|---- FILE: device/ioqueue.c ioq_getchar()
| | 消費者消費一個字符
| | while(ioq_empty(ioq)) // 緩衝區爲空時, 消費者睡眠
| | lock_acquire(&ioq->lock); // 獲取鎖, 每個鎖對應的信號量都會有一個阻塞隊列
| | ioq_wait(&ioq->consumer); // 消費者睡眠
| | lock_release(&ioq->lock); // 釋放鎖
| | char byte = ioq->buf[ioq->tail]; // 消費一個字符
| | if(ioq->producer !=NULL) wakeup(&ioq->producer); // 喚醒生產者(生產者睡眠是因爲緩衝區滿)
|
|---- FILE: device/ioqueue.c ioq_putchar()
| | 生產者生產一個字符
| | while(ioq_full(ioq)) // 緩衝區滿時, 生產者睡眠
| | lock_acquire(&ioq->lock); // 獲取鎖, 每個鎖對應的信號量都會有一個阻塞隊列
| | ioq_wait(&ioq->producer); // 生產者睡眠
| | lock_release(&ioq->lock); // 釋放鎖
| | ioq->buf[ioq->head] = byte; // 生產一個字符
| | if(ioq->consumer !=NULL) wakeup(&ioq->consumer); // 喚醒消費者(消費者睡眠是因爲緩衝區空)
附
物理地址/線性地址/虛擬地址/邏輯地址
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命令),可以把只讀的代碼段共享,沒必要在內存中同時存在多個相同的代碼段。
ret/iret
ret指令(return), 從棧頂彈出2字節來替換ip寄存器。ret只置改變ip, 不改變段基址, 屬於近返回。
iret指令(中斷返回), 從棧中彈出數據到寄存器cs、eip、eflags, 並根據特權級是否改變,判斷是否要恢復舊棧, 即
是否將棧中位於ss_old和esp_old位置的值彈出到寄存ss和esp。