由於工作和興趣愛好的關係,接觸了不少實時操作系統, 一般來說實時操作系統基本沒有進程的概念了,無非是任務堆棧的切換。
一直對Linux,Windows這種帶有進程的OS,很好奇,無奈,LINUX代碼很龐大,很難整體把握。
所以去年一直在尋找帶支持進程的OS, 要求簡單,易懂,確實真找不到。最後找到了MIT教學用VX6,便深深的着迷了。
自從調試了VX6的源代碼,發現用MMU來管理進程真是複雜,怪不得很少能找到支持進程的OS了。
通過參考了很多關於XV6的中文的blog,發現幾乎沒有對進程管理和MMU做闡述的。
於是我將去年的筆記部分公開了出來了(直接從word中黏貼出來的)。
進程
進程的線程 線程堆棧/用戶堆棧
進程的內核線程 內核堆棧
一個進程可能在內核線程中等待IO而導致block
P->pgdir 指向進程的頁表
爲什麼不加載到物理的0地址而是0x100000處,因爲0xa0000:0x100000是IO設備區
第一個進程initcode創建
main
callkvmalloc 設置CPU內核線程的頁表kpgdir
Calluserinit
Callallocproc 初始化 用於進程內核線程執行的 PCB中部分數據結構
調用setupkvm 給進程分配頁表pgdir
調用inituvm
調用kalloc分配一個物理頁mem
將虛擬地址0映射到該物理頁 mappages(0, v2p(mem));爲什麼V2P請看內存管理
將initcode拷貝該物理頁
設置PCB
Tf->eip=0,表示從0地址開始執行
tf->cs 設置爲SEG_UCODE和DPL_USER. tf->ds ES SS設置爲 SEG_UDATA和 DPL_USER
設置的cs會使代碼段的CPL爲3,這樣用戶代碼段只可以訪問PTE_U的頁
tf->eflags = FL_IF允許中斷
tf->esp = PGSIZE 4K
用戶堆棧的空間最大隻有4K??? 由於堆棧由高地址向地址(即由4K向0地址), 用戶代碼和數據都需要佔據空間呀,(代碼從0地址開始)
allocproc
allocproc 創建進程都會調用allocproc
1 從ptable.proc[NPROC]找到一個可用的PCB p
2 調用kalloc給進程內核線程分配物理頁作爲內核堆棧p->kstack,爲什麼是物理內存?此時和邏輯地址什麼關係?
3 設置pid
4 設置內核堆棧
4.1 sp = p->kstack + KSTACKSIZE- sizeof (struct trapframe); p->tf= sp 設置tf的地址
4.2 sp -= 4; *(uint*)sp = trapret 設置堆棧內容
4.3 sp -= sizeof *p->context; p->context= sp 設置context的地址
4.4 p->context->eip = forkret設置堆棧內容
注意整個過程沒有記錄內核的ESP?
從下圖可以看到p->context就是指向ESP,而且 swtch(&cpu->scheduler,proc->context); 參數就是進程ESP
內核堆棧
----------- <-- p->kstack 低地址
| |
| (empty) |
| |
| edi | <-- 第一次爲p->context (內核寄存器)第一個元素
| esi |
| ebx |
| ebp |
| eip | 第一次爲forkret函數地址, p->context的最後一個元素
| | 第一次此處爲trapret(fortret調用結束後的返回的地址).如果是alltraps,此處壓入ESP
| edi |<--p->tf開始(用戶寄存器).alltraps壓棧結束(pushal壓的最後一個reg).trapret手動開始彈出
| esi |
| ebp |
| oesp |
| ebx |
| edx |
| ecx |
| eax | 由於pushal壓入的第一個寄存器
| GS |
| FS |
| ES |
| DS | 此處開始由alltraps壓入
| trapno | 由vectorX壓入的X編號,比如由vector64壓入的是64
| errno | 由vectorX壓入的錯誤號0 trapret手動結束彈出
| EIP | 產生trap 時CPU自動壓入 trapret中iret彈出
| CS | 產生trap 時CPU自動壓入 trapret中iret彈出
| EFLAGS | 產生trap時CPU自動壓入 trapret中iret彈出
| ESP | 用戶模式的ESP.產生trap時CPU自動壓入 如果已經在內核模式,就不會壓入
| SS |<-- p->tf結構體最後.用戶模式的SS.產生trap時CPU自動壓入,如果已經在內核模式,就不會壓入
|---------- | 地址爲p->kstack +KSTACKSIZE 高地址 棧底
問題:p->context中爲什麼要保存這些寄存器? 注意這些寄存器是calleesave register? 被調用函數需要保存這些寄存器,所以需要壓棧
第一個進程initcode運行
main
Userinit
mpmain
Scheduler
switchuvm(p);
swtch(&cpu->scheduler,proc->context); 切換到用戶進程的內核堆棧堆棧
forkret
trapret
用戶空間
調度器scheduler()調用的swtch函數首先esp=p->context,然後從進程內核堆棧恢復進程的內核寄存器,即返回到forkret 函數(p->context->eip)。forkret函數返回到trapret (注意fortret函數中沒有局部變量,即沒有堆棧操作,所以allocproc在設置內核堆棧時候將trapret放在forkret的後面)。Trapret模擬trap(中斷/異常/系統調用)的返回,進而返回到用戶模式(trapret中會彈出EIP等)0地址,寄存器值如下
eax 0x0
ecx 0x0
edx 0x0
ebx 0x0
esp 0x1000
ebp 0x0
esi 0x0
edi 0x0
eip 0x0
eflags 0x202
cs 0x23
ss 0x2b
ds 0x2b
es 0x2b
fs 0x0
gs 0x0
可見代碼段,數據段和堆棧都位於4K之內
第一次系統調用exec
Initcode.s 首先會系統調用exec加載init程序來替換initcode進程的內存,代碼
char init[] = "/init\0";
char *argv[] = { init, 0 };
start:
Push argv
Push init
Push 0 應該是argv的結束符
Eax = 7 (exec系統調用號)
int 64 EXEC系統調用執行init程序
執行int 64之前
eax 0x7
ecx 0x0
edx 0x0
ebx 0x0
esp 0xff4
ebp 0x0
esi 0x0
edi 0x0
eip 0x11
eflags 0x202
cs 0x23
ss 0x2b
ds 0x2b
es 0x2b
fs 0x0
gs 0x0
0xff4處內存爲: 0x00000000 0x0000001c 0x00000024
print cpus[0].proc->kstack 值0x8dfff000
可見內核堆棧使用的ESP=proc->kstack + KSTACKSIZE=0x8dfff000+0x1000=0x8e000000
執行int 64之後(導致進入vector64,建立trapframe,gdb調試已經執行完push 0了)
eax 0x7
ecx 0x0
edx 0x0
ebx 0x0
esp 0x8dffffe8
ebp 0x0
esi 0x0
edi 0x0
eip 0x80106c10
eflags 0x202
cs 0x8
ss 0x10
ds 0x2b
es 0x2b
fs 0x0
gs 0x0
內核堆棧trapframe的內容如下
0x8dffffe8: 0x00000000 (push 0) errorno
0x00000013 eip
0x00000023 CS
0x00000202 eflags
0x8dfffff8: 0x00000ff4 用戶空間esp
0x0000002b 用戶空間ss
0x8e000000:
接下來的sys_exec和exec 都是在這個內核堆棧上。
exec
exec (char *path, char **argv)
主要執行以下內容
1. 讀ELF文件頭,檢測是否有效
2. 調用setupkvm分配頁表pgdir 爲什麼要重新分配一個頁表呢?
3. 分析elf各個段
a) 根據段大小,調用allocuvm爲這些段分配頁,注意仔細看代碼
b) 調用loaduvm加載ELF的段到該段的虛擬地址對應的內核地址(虛擬地址轉換成物理地址,再p2v轉換成內核地址) ,爲什麼不直接放到進程虛擬地址(對應的頁已經分配了啦??),因爲頁表還沒有加載呢。
4. 調用allocuvm分配2個頁,這兩個頁的虛擬地址接着之前的虛擬地址。
5. 調用clearpteu 清除第一個頁的PTE_U標誌(進程空間無法訪問,應該作爲guard page)
6. Sp =sz sz爲整個程序空間的長度包括堆棧,並根據argv 設置當前的堆棧
Init堆棧要如下:
低地址Y: ustack[0] PC=0xffffffff
ustack[1]字符串的個數N
ustack[2] 內容爲char *argv[]的地址X
X: ustack[3+0] 內容爲argv[0]地址
ustack[3+1] 內容爲argv[1]地址
ustack[3+2] 內容爲argv[2]地址
ustack[3+N] 內容爲argv[3+N]地址
ustack[3+N+1 ] 內容爲0
argv[N]整個字符串可能好多字節 要4字節對齊
argv[2]整個字符串可能好多字節 要4字節對齊
argv[1]整個字符串可能好多字節 要4字節對齊
高地址argv[0]整個字符串 可能好多字節 要4字節對齊
可以的得知initcode中init和argv都被拷貝到堆棧了。 從X處應該不會反回的吧,這些都浪費了。。。
但是由於新的頁表還沒有加載,當前堆棧是無法直接訪問的,如何將進程空間的init和argv數據拷貝到堆棧中的呢,請參考 copyout函數
7. 設置path
8. oldpgdir = proc->pgdir; 保存原來的頁表
9. proc->pgdir = pgdir; 更新進程的頁表
10. proc->sz = sz; 重新設置進程整個空間的大小
11. proc->tf->eip =elf.entry; 設置eip的入口
12. proc->tf->esp = sp; 設置堆棧sp,sp的值爲地址Y
13. switchuvm(proc);
14. freevm(oldpgdir); 並返回
進程調度
上下文切換
從進程的內核線程 切換到 CPU的調度線程/scheduler thread
從CPU的調度線程切換到 新進程的內核線程
CPU的調度線程/CPU內核線程 用於執行調度器
Scheduler
得到狀態RUNABLE的進程proc
Switchuvm(proc) 加載進程的頁表
設置狀態爲RUN
Swtch(&cpu->scheduler,proc->context) 由CPU內核線程切換到進程的內核線程
Switchkvm
swtch
進程的內核線程會調用swtch來保存上下文(內核堆棧和內核寄存器) 並返回到 調度器的上下文。
即swtch用於切換內核堆棧和內核寄存器
Swtch調用代碼如下:
extern struct cpu *cpuasm("%gs:0"); // Thiscpu.
extern struct proc *procasm("%gs:4"); // Currentproc on this cpu.
swtch(&cpu->scheduler,proc->context);
返彙編如下:
mov %gs:0x4,%eax proc的內容eax
mov 0x1c(%eax),%eax proc->context -> EAX
mov %eax,0x4(%esp) proc->context 壓入堆棧
mov %gs:0x0,%eax
add $0x4,%eax
mov %eax,(%esp) &cpu->scheduler 壓入堆棧
call swtch
swtch: // swtch(struct context **old,struct context *new);
movl 4(%esp), %eax ;eax內容爲&cpu->scheduler
movl 8(%esp), %edx ;edx 內容爲proc->context
pushl %ebp
pushl %ebx
pushl %esi
pushl %edi
movl %esp, (%eax)
;將當前內核堆棧ESP壓入
cpu->scheduler中
cpu->scheduler=esp
movl %edx, %esp ;將proc->context 賦值 當前內核堆棧ESP esp = proc->context
popl %edi
popl%esi
popl %ebx
popl %ebp
ret
問題:
Swtch 保存cpu內核線程的寄存器,然後就沒有了。
如果下次切換到CPU內核線程,會出現什麼情況呢?
因爲剛開始CPU內核線程使用的堆棧爲stack, swtch只是將一些寄存器保存到cpu->scheduler沒有保存ESP.
而CPU內核線程恢復時候,直接從cpu->scheduler上恢復寄存器,還能正確找到原來的堆棧stack嗎?
回答:
其實swtch已經保存了esp: 即 cpu->scheduler =esp, 即cpu->scheduler指向CPU內核線程的內核堆棧棧頂
下次CPU內核線程恢復運行時,esp=cpu->cheduler,即可從CPU內核堆棧上恢復寄存器
總結
CPU內核線程/調度線程
執行 scheduler()
1. 選擇就緒的進程p, proc =p
2. switchuvm(proc)
3. swtch(&cpu->scheduler,proc->context)
4. switchkvm
5. proc = 0, 從1開始重新執行
永遠不會返回
可見proc表示當前進程的PCB
第一個進程的運行
mainc
調用mpmain
調用scheduler
調用swtch 保存CPU內核線程的ESP和寄存器,並切換到一個進程的的內核線程
返回到forkret
返回到trapret
返回到用戶進程
時間片輪轉調度
定時中斷 T_IRQ0 + IRQ_TIMER=32
情況1:普通情況,進程運行過程中產生定時中斷
vector32 jmp alltraps
trap
walkup
yield
sched
swtch 切換到CPU的內核線程
返回到scheduler()的switchkvm,選擇kpgdir作爲頁表,
繼續執行的scheduler()的第5步驟,又開始執行scheduler()的第1,2,3步驟(選擇新的進程)
執行scheduler():swtch 切換到新進程的內核寄存器和內核堆棧
返回到新進程sched():swtch函數調用的下條指令
返回到yield
返回到trap
返回到alltraps
返回到新進程用戶空間
即
進程調用Sched(),sched調用swtch切換到CPU調度線程
(先保存進程的寄存器和swtch函數的返回地址)
CPU調度線程調用Sched(),sched調用swtch切換到進程
(先保存CPU調度進程的寄存器和swtch函數的返回地址)
其實可以看到scheduler函數和sched函數是互相切換運行,又稱co-routines
當CPU處於IDLE狀態(沒有可運行的進程), IDLE狀態的CPU的scheduler函數一開始就上鎖, 正在運行進程的其他cpu無法實施上下文切換或者進程相關的系統調用,也無法設置一個進程爲可運行以至於讓IDLECPU跳出調度循環。
接着關於lock,沒看懂
情況2:在 cpu內核線程運行時(在執行scheduler()函數),產生定時中斷
vector32
Alltraps
Trap但是,proc爲0,
直接退出中斷
額外說明:
比如定時中斷髮生在 情況1某個步驟,會不會產生中斷嵌套呢?
應該不會,因爲執行了swtch之後,內核堆棧切換到CPU內核堆棧上了,而中斷使用的堆棧是什麼?
經過調試發現,進入中斷時候ESP有時0XFFFFB4(B4+4C(trapframe結構體的長度,由中斷自行壓入)),應該打斷CPU內核線程
有時0XFF1D60,應該是打斷了進程內核線程 ,第一個進程初始化的時 kstack爲0xff1000,內核SP爲0Xff2000,那爲什麼不是0XFF1FB4呢?感覺是嵌套了,估計: 在內核模式下運行(中斷,系統調用,異常,反正都是trap)是trap模式,ESP已經從TSS中取出,如果在內核模式下再出現中斷,那麼不會重新從TSS取ESP。
估計正確,根據某次調試結果:
進入vector32 ,esp爲0XFF0D60,強制跳過yield,vector32返回後,ESP爲0xff0D70,即壓入16個byte,4個寄存器
(根據文檔是errno, EIP, CS, EFLAGS ,但是errorno不是由VECTOR32壓入的嗎?
調試:
某次ESP爲0xff0dc0
0xff0dc0: 0x103cf0(EIP) 0X08(CS)0X202(EFLAGS) 0X10140C()
向量返回後 ESP爲0xff0dcc 正好是彈出了3個寄存器
LOCKING
內存管理
X86頁式管理
頁表/PAGE TABLE
存放頁表項, 有2^20=1M個頁表項/PAGE TABLEENTRY/PTE,
每個頁表項指向物理頁面4K 所以一個頁表能表示4K*1M=4G
在頁表中每個頁表項佔用4BYTE,所以一個頁表佔用4*1M=4M byte
PTE
一個PTE 中,PHYSICAL PAGE NUMBER /PPN佔用20bit 的
線性地址的高20bit,低12bit作爲頁內偏移地址
X86的頁表具體如下:
一個頁表Page DirectoryTable/PDT 包好 1024 個 Page Table Page/PTP/Page Directory Entry
每個PDP 包含1024 個 32bit的PTE
分頁硬件將虛擬地址 頭10bit 用於指定PDE,如果該PDE的PTE_P置位,繼續,否則fault
分頁硬件將虛擬地址中間10bit 用於指定PTE,如果該PTE的PTE_P置位,繼續,否則fault
線性地址轉換原理,
CPU得到一個虛擬地址後,首先通過段式管理得到線性地址
取出線性地址的高20bits後,
CR3 頁表寄存器
頁表項/PTE=頁表基地址/PT+(線性地址>>20)
物理頁面=頁表項的高20bit
物理地址 = 物理頁面+線性地址的低12bits
每個PTE中含有標誌位來表示對應的線性地址的訪問權限
PTE_P 表示當前PTE是否有效,如果訪問無效的PTE會導致fault
PTE_W 可寫,不可寫的話只能讀和執行
PTE_U 用戶程序可以訪問,否則只能內核程序訪問
每個進程都有一個獨立的頁表,進程切換時候需要切換頁表.
VX6內存管理
不同進程將用戶空間轉換到不同的物理頁,所以進程有私有的用戶空間,進程空間從0地址開始
每個進程的頁表都映射內核空間(KERNBASE以上的地址),內核空間的PTE不會設置PTE_U
即虛擬地址XUKERNBASE~KERNBASE+PHYSTOP映射到0~PHYSTOP。理由是
1. 內核可以使用自己的代碼和數據,不明白什麼意思?
2. 內核有時會寫物理page. 比如分配頁表時,物理地址如果映射可以預測的虛擬地址,就很方便了。不明白?
猜想: 假設內核給某進程的虛擬地址0X0分配的物理頁肯定在0~PHYSTOP範圍內,那麼內核可以直接訪問該地址(位於XUKERNBASE~KERNBASE+PHYSTOP範圍), 也可以該進程空間訪問0x0(實際是通過頁表通過訪問該物理頁)
缺點:
Xv6不能使用超過2G的物理內存. WINXP好像也不能使用2G的內存的。
每個進程的頁表包含的用戶空間(進程空間)和內核空間的映射。這樣系統調用和中斷就不用切換頁表。
Xv6保證每個進程使用獨立的用戶空間(利用PTE_U實現), 保證每個進程虛擬地址連續並且開始於0地址.(利用頁表將連續的虛擬地址映射到不連續的物理頁上)
地址空間
PA物理地址地址範圍 0~PHYSTOP
UVA進程空間虛擬地址 0~KERNBASE
KA內核空間虛擬地址 KERNBASE~ KERNBASE+PHYSTOP其中
KERNBASE~KERNBASE+EXTMEMBIOS /IO
KERNBASE+EXTMEM~KERNBASE+PHYSTOP 映射在 0~PHYSTOP
KERNBASE+EXTMEM ~ data xv6內核的text .rodata段
data ~ end xv6內核的data段BSS段
end~ KERNBASE+PHYSTOP 分配其他用(如用戶空間)
注意不管怎麼樣,進程空間虛擬地址和內核空間虛擬地址都是應設在物理地址0~PHYSTOP
進程空間虛擬地址轉換爲內核空間虛擬地址方法:
進程空間虛擬地址->進程頁表->物理地址
內核空間虛擬地址=p2v(物理地址) ,當然內核空間也可以用進程空間虛擬地址來訪問
物理頁分配
內核從end~ KERNBASE+PHYSTOP這個內核空間虛擬地址範圍內來分配頁,所以這個頁地址還是虛擬地址.
一般來說,這個頁可以拿來直接使用了。但是什麼情況下需要轉換爲物理地址:
當要生成頁表的物理映射的時候,就需要v2p轉換爲物理地址
mem = kalloc();分配頁
if(mappages(頁表, 虛擬地址, 地址範圍, v2p(mem)物理地址) 轉換爲物理地址
關於entry
所有的物理地址映射好之後, 分配器纔可以初始化了freelist。但是創建一個頁表(並初始化頁表內的映射)需要分配器來分配物理頁。 ---太費解了。。。
Xv6在entry是利用獨立的分配器來解決這個問題,儘管不支持釋放頁,受限於4M,但是足夠分配一個內核頁表kpgdir
Entry 使用的頁表是entrypgdir,頁大小爲4M (cr4|=CR4_PSE)
問題1:進入main程序特別是kvmalloc之後,使用的4K頁, 這是如何區分的呢?
在 CR4.PSE 置爲 1 時,將開啓 4M page,但在具體由相應的 page-translation tables 的屬性來決定哪個是 4M pages,哪個是 4K pages。
查看 entrypgdir 數組一個元素 [0] = (0)| PTE_P | PTE_W | PTE_PS, PTE_PS應該就表示4M的PAGE
Main
kinit1(end,P2V(4*1024*1024)); end爲.text.data.bss之後的地址, P2V(4*1024*1024)爲0X80400000
kvmalloc
kinit2(P2V(4*1024*1024),P2V(PHYSTOP)); 0X80400000
爲什麼要兩次調用?
Kinit2 使能locking
物理內存管理
kinit1/ kinit2
kinit1/ kinit2(void *vstart, void *vend)
freerange(vstart,vend)
freerange
freerange(void *vstart, void *vend)
將內核空間虛擬地址從vstart到vend這個範圍以4K爲單位連接起來 kmem.freelist
Kalloc
從kmem.freelist分配一個頁, 注意該頁使用的是內核空間虛擬地址,要使用V2P才能轉換成真正的物理地址。
沒有特殊說明的話,就直接說明用kalloc函數分配一個物理頁
虛擬內存管理/用戶空間
Heap爲stack之上,可以使用sbrk擴展
Stack佔用一個page,內容如下圖.
Xv6在stack下面有一個guard page(未映射),當stack溢出時(堆棧向下增長,使用guard page),導致頁未映射異常。
allocuvm
allocuvm(pde_t *pgdir, uint oldsz, uintnewsz) 將進程空間內存由oldsz擴展到newsz
即需要額外分配 newsz-oldsz 空間
基本原理
1. 調用kalloc分配4K物理頁, 將oldsz開始的4K映射到該物理頁(這樣進程的虛擬地址就連續了)
2. oldsz+=4K
3. oldsz < newsz? 是,走1,否則退出
loaduvm
loaduvm(pde_t *pgdir, char *addr, structinode *ip, uint offset, uint sz) 加載elf段到用戶空間
1. 判斷addr是否4k對齊
2. 根據addr調用walkpgdir尋找對應頁的pte
3. 根據pte計算物理地址pa
4. 根據ip,offset(段在elf文件中的偏移),長度調用readi讀取信息到 內核空間虛擬地址p2v(pa)
爲什麼不直接讀到addr,請看exec
copyout
copyout(pde_t *pgdir, uint va, void *p,uint len) 從地址p處拷貝數據到va(包含va地址映射的頁表pgdir非當前進程頁表)
1. pa =uva2ka(pgdir, (char*)va);根據pgdir計算va的pa(其實是內核空間虛擬地址)
2. 將p處數據拷貝到pa(因爲是內核空間虛擬地址,所以內核可以訪問)
uva2ka(pde_t *pgdir, char *uva)
1. 調用walkpgdir得到uva的pte
2. 該pte 的 PTE_P和PTE_U必須存在
3. p2v(PTE_ADDR(*pte)) 根據PTE計算得到物理地址,在轉換內核空間虛擬地址
虛擬內存管理/內核
鏈接腳本中定義了鏈接地址data (text段結束地址) end(bss段結束)
Kpgdir CPU內核線程使用的頁表
setupkvm
調用kalloc分配一個物理頁
調用mappages 設置頁表,利用kmap映射區域如下
virt; phys_start; phys_end; perm; kmap[] = {
KERNBASE, 0, EXTMEM, PTE_W // I/O space
KERNLINK, V2P(KERNLINK), V2P(data),0}, // kern text+rodata
data, V2P(data), PHYSTOP, PTE_W}, // kern data+memory
DEVSPACE, DEVSPACE, 0, PTE_W}, // more devices
虛擬地址到物理地址
虛擬地址0~KERNBASE即用戶空間(text+data+stack+heap),對應的物理內存有內核來分配
虛擬地址KERNBASE~KERNBASE+EXTMEM映射到0~EXTMEM即I/O地址空間 BIOS
虛擬地址KERNBASE+EXTMEM~data映射到EXTMEM~V2P(data) 內核text和ro段
虛擬地址data~KERNBASE+PHYSTOP映射到V2P(data)~PHYSTOP,內核data段BSS段和可供內核分配的內存
虛擬地址0xfe000000~0直接映射 (devices such as ioapic)
物理地址到虛擬地址
0~EXTMEM即I/O地址空間 對應虛擬地址KERNBASE~KERNBASE+EXTMEM
EXTMEM~V2P(data) 對應虛擬地址KERNBASE~KERNBASE+EXTMEM內核text和ro段
V2P(data)~PHYSTOP對應虛擬地址data~KERNBASE+PHYSTOP內核data,BSS段和可供內核分配的內存
0xfe000000~0 對應虛擬地址0xfe000000~0 (devices such as ioapic)
物理地址V2P(end)~PHYSTOP(對應虛擬地址end~P2V(PHYSTOP)),這段內存內核用於分配內存等(包括用戶空間)
Kvmalloc
kpgdir =setupkvm();
switchkvm();
switchkvm
使用v2p(kpgdir)作爲CPU內核線程頁表
Switchuvm
設置TSS的ss寄存器
設置TSS的esp爲proc->kstack +KSTACKSIZE 即設置內核堆棧esp
加載該TSS段
加載進程頁表pgdir
Trap處理
中斷由內核來處理
進入ISR要保存寄存器, 從用戶模式切換內核模式
退出ISR要恢復寄存器, 從內核模式切換用戶模式
4級保存0~3, 內核模式使用0,用戶模式使用3,當前的保護級別位於CS的CPL字段內
中斷子程序定義在IDT中,IDT中256的條目,每條包含CS,eip信息
0-31爲軟件中斷
32-63 硬件中斷 INTERRUPT GATE 清除EFLAG的FL標誌 32 定時中斷
64 系統調用 TRAP GATE,即不會清除EFLAG的FL標誌, 系統調用中允許中斷。DPL_USER。
不允許app調用其他中斷(比如 int 非系統調用號),否則會進入異常 VECTOR[13].
Int n調用系統調用,n表示中斷子程序在IDT的索引。運行系統調用指令,CPU的步驟如下:
1. 從IDT中取出第n個索引的描述符
2. 檢查CS中的CPL <= DPL, DPL在描述符中
3. 如果目標段選擇字的PL <CPL,保存ESP和ss 到CPU內部寄存器中
4. 從TSS/任務段描述符加載ss和ESP(內核模式的)
5. 壓棧原來的ss,原來的ESP (即用戶模式的ESP,SS)
6. 壓棧EFLAGS,CS,EIP,error(視情況),
7. 清空eflags的某些位,根據IDT描述符來設置CS,EIP
如果CPU在運行內核模式(已經在特權模式),不會做5步驟中壓棧SS,ESP
中斷使用的堆棧是內核堆棧。 TSS 在哪裏設置的?switchuvm中,
每個進程都會公用TSS段,但是不同進程時,設置該TSS段的爲進程的內核堆棧
Trap發生, CPU如果在內核模式,不需要做4,5步驟
系統調用
系統調用函數的代碼如下:
exec:
mov $0x1,%eax
int 64
ret
Vector64 jmp alltraps
Trap
Syscall
sys_exec
獲得系統調用參數
exec
alltraps中保存完用戶寄存器後,開始設置DS,eS爲內核數據段,之後都是內核操作啦,壓入esp(指向當前的trap frame)作爲C函數trap的參數,然後調用trap
trap
如果是系統調用,首先從proc->tf->eax獲得系統調用號 (EAX=7是exec)
執行系統調用(首先獲得系統調用的參數)
將系統調用的返回值賦值給proc->tf->eax,這樣作爲系統調用函數的返回值返回給用戶空間
Fork
*np->tf = *proc->tf; 拷貝整個trapframe
中斷
設置eflag中FL會允許中斷
CLI 會清空EFLAGS中FL
STI會設置EFLAGS中FL
定時中斷IRQ 0 使用vector32向量
Vector32 jmp alltrap
Trap
wakeup