lab4 是實(shí)現(xiàn)多處理器支持以及搶占式任務(wù)調(diào)度剧罩,exercize代碼見 這里。
1 多處理器啟動(dòng)流程
1.1 多處理器支持
為了支持多處理器一膨,首先需要知道多處理器的配置呀邢,這個(gè)配置通常是存儲(chǔ)在BIOS里面。BIOS需要傳遞配置信息給多個(gè)處理器豹绪,同時(shí)需要能復(fù)原多處理器及其相關(guān)組件价淌,多處理器的BIOS也要擴(kuò)展功能,增加MP配置瞒津。
SMP是指所有處理器都是平等的蝉衣,包括內(nèi)存對(duì)稱和IO對(duì)稱。內(nèi)存對(duì)稱指所有處理器都共享同樣的內(nèi)存地址空間巷蚪,訪問(wèn)相同的內(nèi)存地址病毡。而IO對(duì)稱則是所有處理器共享相同的IO子系統(tǒng)(包括IO端口和中斷控制器),任一處理器可以接收任何源的中斷屁柏。雖然處理器都平等的啦膜,但是可以分為BSP(啟動(dòng)處理器)和AP(應(yīng)用處理器),BSP負(fù)責(zé)初始化其他處理器淌喻,至于哪個(gè)處理器是BSP則是由BIOS配置決定的僧家。
APIC(Advanced Programmable Interrupt Controller)基于分布式結(jié)構(gòu),分為兩個(gè)單元裸删,一個(gè)是處理器內(nèi)部的Local APIC單元(LAPIC)啸臀,另一個(gè)是IO APIC單元,它們兩個(gè)通過(guò)Interrupt Controller Communications (ICC) 總線連接。APIC作用一是減輕了內(nèi)存總線中關(guān)于中斷相關(guān)流量乘粒,二是可以在多處理器里面分擔(dān)中斷處理的負(fù)載。
LAPIC提供了 interprocessor interrupts (IPIs),它允許任意處理器中斷其他處理器或者設(shè)置其他處理器伤塌,有好幾種類型的IPIs灯萍,如INIT IPIs和STARTUP IPIs。每個(gè)LAPIC都有一個(gè)本地ID寄存器每聪,每個(gè)IO APIC都有一個(gè) IO ID寄存器旦棉,這個(gè)ID是每個(gè)APIC單元的物理名稱,它可以用于指定IO中斷和interprocess中斷的目的地址药薯。因?yàn)锳PIC的分布式結(jié)構(gòu)绑洛,LAPIC和IO APIC可以是獨(dú)立的芯片,也可以將LAPIC和CPU集成在一個(gè)芯片童本,如英特爾奔騰處理器(735\90, 815\100)真屯,而IO APIC集成在IO芯片,如英特爾82430 PCI-EISA網(wǎng)橋芯片穷娱。集成式APIC和分離式APIC編程接口大體是一樣的绑蔫,不同之處是集成式APIC多了一個(gè)STARTUP的IPI。
在SMP系統(tǒng)中泵额,每個(gè)CPU都伴隨有一個(gè)LAPIC單元配深,LAPIC用于傳遞和響應(yīng)系統(tǒng)中斷,LAPIC也為與它連接的CPU提供了一個(gè)唯一ID弄慰,在lab4中雀监,我們只用到LAPIC的一些基本功能:
- 讀取LAPIC標(biāo)識(shí)來(lái)告訴我們當(dāng)前代碼運(yùn)行在哪個(gè)CPU上(見cpunum())肋僧。
- 從BSP發(fā)送 STARTUP IPI到AP,用于啟動(dòng)AP缸托,見(lapic_startup())。
- 在part C锥腻,我們變成LAPIC內(nèi)置的計(jì)時(shí)器來(lái)觸發(fā)時(shí)鐘中斷支持搶占式多任務(wù)(見apic_init())嗦董。
處理器訪問(wèn)它的LAPIC使用的是 MMIO,在MMIO里瘦黑,一部分內(nèi)存硬連線到了IO設(shè)備的寄存器京革,因此用于訪問(wèn)內(nèi)存的load/store指令可以用于訪問(wèn)IO設(shè)備的寄存器。比如我們?cè)趯?shí)驗(yàn)1中用到 0xA0000開始的一段內(nèi)存作為VGA顯示緩存幸斥。LAPIC所在物理地址開始于0xFE000000(從Intel的文檔和測(cè)試看這個(gè)地址應(yīng)該是0xFEE00000)匹摇,在JOS里面內(nèi)核的虛擬地址映射從KERNBASE(0xf00000000)來(lái)說(shuō),這個(gè)地址太高了甲葬,于是在JOS里面在MMIOBASE(0xef800000)地址處留了4MB空間用于MMIO廊勃,后面實(shí)驗(yàn)會(huì)用到更多的MMIO區(qū)域,為此我們要映射好設(shè)備內(nèi)存到MMIOBASE這塊區(qū)域,這個(gè)過(guò)程有點(diǎn)像boot_alloc
坡垫,注意映射范圍判斷梭灿。接下來(lái)完成作業(yè)1,見代碼冰悠。
* MMIOLIM ------> +------------------------------+ 0xefc00000 --+
* | Memory-mapped I/O | RW/-- PTSIZE
* ULIM, MMIOBASE --> +------------------------------+ 0xef800000
1.2 AP啟動(dòng)流程
在啟動(dòng)AP前堡妒,BSP首先要收集多處理器系統(tǒng)的信息,比如CPU數(shù)目溉卓,CPU的APIC ID和LAPIC單元的MMIO地址皮迟。kern/mpconfig.c
的mp_init()函數(shù)通過(guò)讀取駐留在BIOS內(nèi)存區(qū)域中的MP配置表來(lái)獲取這些信息。
boot_aps()函數(shù)驅(qū)動(dòng)AP啟動(dòng)進(jìn)程桑寨。AP以實(shí)模式啟動(dòng)伏尼,很像boot/boot.S
中那樣,boot_aps()將AP entry代碼拷貝到實(shí)模式可尋址的一個(gè)地址尉尾,與bootloader不同的是爆阶,我們可以控制AP開始執(zhí)行代碼的位置,我們將AP entry代碼拷貝到 0x7000(MPENTRY_PADDR)代赁,當(dāng)然其實(shí)你拷貝到640KB之下的任何可用的按頁(yè)對(duì)齊的物理地址都是可以的扰她。
之后,boot_aps()通過(guò)向AP的LAPIC發(fā)送STARTUP IPIs依次激活A(yù)P芭碍,并帶上AP要執(zhí)行的初始入口地址CS:IP(MPENTRY_PADDR)徒役。入口代碼在 kern/mpentry.S,跟boot/boot.S
非常相似窖壕。在簡(jiǎn)單的設(shè)置后忧勿,它將AP設(shè)置為保護(hù)模式,并開啟分頁(yè)瞻讽,然后調(diào)用 mp_main()里面的C設(shè)置代碼鸳吸。boot_aps()會(huì)等待AP在其CpuInfo中的cpu_status字段發(fā)出CPU_STARTED 標(biāo)志,然后繼續(xù)喚醒下一個(gè)AP速勇。為此需要將 MPENTRY_PADDR 這一頁(yè)內(nèi)存空出來(lái)晌砾。
接下來(lái)我們要分析下加入多處理器支持后JOS的啟動(dòng)流程,新加的幾個(gè)相關(guān)函數(shù)是 mp_init()
, lapic_init()
以及boot_aps()
烦磁。
多處理器配置搜索和初始化
mp_init()主要是搜索多處理器配置信息养匈,要怎么找呢,首先是按下面的順序找MP Floating Pointer Structure
(簡(jiǎn)寫為MPFPS)都伪。
- 1 去Extended BIOS Data Area (EBDA)的前1KB處
- 2 去系統(tǒng)base memory的最后1KB找
- 3 去BIOS的只讀內(nèi)存空間: 0xE0000 和 0xFFFFF 之間找(代碼里面用的是 0xF0000 到0xFFFFF位置)呕乎。
+------------------+ <- 0x00100000 (1MB)
| BIOS ROM |
+------------------+ <- 0x000F0000 (960KB)
| 16-bit devices, |
| expansion ROMs |
+------------------+ <- 0x000C0000 (768KB)
| VGA Display |
+------------------+ <- 0x000A0000 (640KB)
| |
| Low Memory |
| |
+------------------+ <- 0x00000000
而EBDA的起始位置可以從 BIOS 的 40:0Eh 處找到,也就是 0x40 << 4 + 0x0Eh = 0x40Eh 處找EBDA的起始位置陨晶。在測(cè)試中猬仁,我的測(cè)試機(jī)里面顯示該值為 0x9fc0
,故而會(huì)先在EBDA的 0x9fc00
(左移4位得到物理地址) 到 0xA0000之間找。在BIOS 的 40:13h 處可以找到base memory大小值減1KB的值湿刽,這個(gè)值是 以KB為單位的的烁,比如我的測(cè)試環(huán)境顯示該值為 0x9fc00,則base memory為0x9fc00 + 1K = 0xA0000 也就是640KB叭爱。由此我們這里EBDA的前1KB和base memory的最后1KB其實(shí)是同一個(gè)內(nèi)存區(qū)域撮躁。如果前面兩個(gè)位置找不到,則會(huì)去0xE0000h到0xFFFFFh區(qū)域找买雾。在測(cè)試環(huán)境中在位置3找到了MPFPS,這里的校驗(yàn)方式是 mp->signature == "MP" 且mp結(jié)構(gòu)體的所有字段之和為0杨帽。
找到了MPFPS后漓穿,我們要根據(jù)它的配置去找 MP Configuration Table
(MPCT),發(fā)現(xiàn) MPFPS中的 physical address
值為 0xf64d0注盈,即表示 MPCT地址在0xf64d0開始的一段BIOS ROM里面晃危。可以調(diào)試發(fā)現(xiàn)我們測(cè)試機(jī)里面的MPCT的版本為1.4老客,LAPIC的基地址為 0xfee00000
僚饭,配置項(xiàng)有20個(gè),而這里的配置項(xiàng)又分為 Processor, Bus胧砰,IO APIC鳍鸵,IO Interrupt Assignment以及Local Interrupt Assignment
這五種類型。對(duì)于處理器類型尉间,這里有幾個(gè)比較重要的字段偿乖,其中有cpu的幾個(gè)標(biāo)識(shí),其中一個(gè)BP如果設(shè)置為1哲嘲,表示這個(gè)處理器是啟動(dòng)處理器BSP贪薪。另一個(gè)是EN,為1表示啟用眠副,為0表示禁用画切。還有一個(gè)LAPIC ID字段,用于標(biāo)識(shí)該處理器里面的LAPIC的ID囱怕,ID是從0開始編號(hào)的霍弹。JOS里面最多支持8個(gè)CPU,多余的CPU不會(huì)啟用光涂。
在我們測(cè)試的時(shí)候make qemu CPUS=n
庞萍,其中的n就是指定的模擬的CPU的數(shù)目,指定了幾個(gè)我們就能找到幾個(gè)CPU的MPCT配置項(xiàng)忘闻。為維護(hù)CPU狀態(tài)钝计,JOS內(nèi)核中維護(hù)了一個(gè)cpus的數(shù)組和CpuInfo結(jié)構(gòu)體。
// Per-CPU state
struct CpuInfo {
uint8_t cpu_id; // Local APIC ID; index into cpus[] below
volatile unsigned cpu_status; // The status of the CPU
struct Env *cpu_env; // The currently-running environment.
struct Taskstate cpu_ts; // Used by x86 to find stack for interrupt
};
// Initialized in mpconfig.c
extern struct CpuInfo cpus[NCPU];
找到配置后,接著會(huì)設(shè)置BSP為falg為BP的處理器私恬,并將其狀態(tài)設(shè)置為 CPU_STARTED债沮。接下來(lái)開始初始化LAPIC。
lapic_init()
因?yàn)長(zhǎng)APIC的起始地址默認(rèn)是在物理地址0XFEE00000本鸣,為了方便訪問(wèn)疫衩,JOS將這地址通過(guò)MMIO映射到了虛擬地址MMIOBASE。映射完成后荣德,我們就可以用lapic這個(gè)虛擬地址來(lái)訪問(wèn)和設(shè)置LAPIC了闷煤。lapic_init()主要對(duì)LAPIC的一些寄存器進(jìn)行設(shè)置,包括設(shè)置ID涮瞻,version鲤拿,以及禁止所有CPU的NMI(LINT1),BSP的LAPIC要以Virtual Wire Mode運(yùn)行署咽,開啟BSP的LINT0近顷,以用于接收8259A芯片的中斷等。
pic_init()
pic_init()用于初始化8259A芯片的中斷控制器宁否。8259A芯片是一個(gè)中斷管理芯片窒升,中斷來(lái)源除了來(lái)自硬件本身的NMI中斷以及軟件的INT n指令造成的軟件中斷外,還有來(lái)自外部硬件設(shè)備的中斷(INTR)慕匠,這些外部中斷時(shí)可以屏蔽的饱须。而這些中斷的管理都是通過(guò)PIC(可編程中斷控制器)來(lái)控制并決定是否傳遞給CPU,JOS中開啟的INTR中斷號(hào)有1和2絮重。
boot_aps()
接下來(lái)是啟動(dòng)AP了冤寿。首先通過(guò)memmove將mpentry的代碼拷貝到 MPENTRY_PADDR (0x7000)處(其中習(xí)題2要將0x7000對(duì)應(yīng)的一頁(yè)設(shè)置為已用,不要加入到空閑鏈表)青伤,設(shè)置好對(duì)應(yīng)該cpu的堆棧棧頂指針督怜,然后調(diào)用kern/lapic.c
中的lapic_startap()
開始啟動(dòng)AP。
lapic_startap()完成lapic的設(shè)置狠角,包括設(shè)置warm reset vector指向mpentry代碼起始位置号杠,發(fā)送STARTUP IPI以觸發(fā)AP開始運(yùn)行mpentry代碼,并等待AP啟動(dòng)完成丰歌,一個(gè)AP啟動(dòng)完成后再啟動(dòng)下一個(gè)姨蟋。
那么mpentry代碼就是在kern/mpentry.S
中了,它的作用類似bootloader立帖,最后是跳轉(zhuǎn)到mp_main()函數(shù)執(zhí)行初始化眼溶。mpentry.S 在加載GDT和跳轉(zhuǎn)時(shí)用到了MPBOOTPHYS宏定義,因?yàn)閙pentry.S代碼是加載在 KERNBASE之上的晓勇,在CPU實(shí)模式下是無(wú)法尋址到這些高地址的堂飞,所以需要轉(zhuǎn)換為物理地址灌旧。而boot.S代碼不用轉(zhuǎn)換,是因?yàn)樗鼈儽旧砭图虞d在實(shí)模式可以尋址的0x7c00-0x7dff
绰筛。后面的流程跟boot.S類似枢泰,也是開啟保護(hù)模式和分頁(yè)。因?yàn)閙pentry的代碼加載到了 0x7000铝噩,需要在 page_init()
中將這一頁(yè)從page空閑鏈表去除衡蚂,見作業(yè)2.
#define MPBOOTPHYS(s) ((s) - mpentry_start + MPENTRY_PADDR)
此時(shí)用的頁(yè)目錄跟 kern/entry.S
時(shí)一樣,用的也是 entry_pgdir骏庸,因?yàn)榇藭r(shí)的運(yùn)行指令在低地址毛甲,并沒有在 kern_pgdir
建立映射。 最后通過(guò)call
指令跳轉(zhuǎn)到mp_main()函數(shù)執(zhí)行具被,注意下這里用了間接call丽啡,為什么不是直接call $mp_main
呢? 這里之所以不直接call,是因?yàn)橹苯觕all用的是相對(duì)地址硬猫,即將 EIP 設(shè)置為 EIP + call后跟的一個(gè)相對(duì)地址
,比如這里我們的call指令的地址為0x7050
改执,然后EIP會(huì)指向下一條地址0x7055啸蜜,call地址會(huì)被設(shè)置為 0x7050 + 5 + 0xffffa609 = 0x10000165e
,地址溢出后變成0x165e
辈挂,而這個(gè)地址內(nèi)容是0x80050044
衬横,可知0x165e處對(duì)應(yīng)的指令是 0x44,也就是 inc %esp
终蒂,當(dāng)然這一步不會(huì)報(bào)錯(cuò)蜂林,接著下一條指令在 0x165f,指令對(duì)應(yīng)的是 00 05 80 44 00 05
拇泣,即add %al, 0x05004480
噪叙,則此時(shí)訪問(wèn)地址0x05004480
會(huì)報(bào)錯(cuò),因?yàn)榇藭r(shí)用的是entry_pgdir
霉翔,還沒有建立該地址的頁(yè)面映射睁蕾。
## 正確方式
mov $mp_main, %eax;
call *%eax;
## 錯(cuò)誤方式
call mp_main
f0105bc8: e8 09 a6 ff ff call f01001d6 <mp_main>
(gdb) x /16x 0x165e
0x165e: 0x80050044 0x90050044 0xa0050044 0xb0050044
mp_main()函數(shù)先是加載了kern_pgdir到CR3中,然后調(diào)用下面幾個(gè)方法债朵,包括前面提過(guò)的lapic_init(),以及為每個(gè)CPU初始化進(jìn)程相關(guān)內(nèi)容和中斷相關(guān)內(nèi)容子眶,最后設(shè)置cpu狀態(tài)為 CPU_STARTED 讓 BSP 去啟動(dòng)下一個(gè)CPU。注意到這里用到了xchg函數(shù)來(lái)設(shè)置cpu狀態(tài)序芦,該函數(shù)用到xchgl來(lái)交換addr存儲(chǔ)的值和newval臭杰,并將addr原來(lái)存儲(chǔ)的值存到result變量中返回,指令中的lock;
用于保證多處理器操作的原子性谚中。
void
mp_main(void)
{
// We are in high EIP now, safe to switch to kern_pgdir
lcr3(PADDR(kern_pgdir));
lapic_init();
env_init_percpu();
trap_init_percpu();
xchg(&thiscpu->cpu_status, CPU_STARTED); // tell boot_aps() we're up
for (;;);
}
static inline uint32_t
xchg(volatile uint32_t *addr, uint32_t newval)
{
uint32_t result;
// The + in "+m" denotes a read-modify-write operand.
asm volatile("lock; xchgl %0, %1"
: "+m" (*addr), "=a" (result)
: "1" (newval)
: "cc");
return result;
}
1.3 CPU初始化
多核CPU需要各自優(yōu)化渴杆,每個(gè)CPU都有自己的一些初始化變量寥枝,如下:
內(nèi)核棧
每個(gè)cpu都要有一個(gè)內(nèi)核棧,以免互相干擾将塑。percpu_kstacks[NCPU][KSTKSIZE]
用于保存椔龆伲空間。
TSS和TSS描述符
每個(gè)CPU都要有自己的TSS(任務(wù)狀態(tài)段)和TSS描述符点寥。CPU i的TSS存儲(chǔ)在cpus[i].cpu_ts
艾疟,而TSS描述符在GDT中的索引是gdt[(GD_TSS0 >> 3) + i]
,之前實(shí)驗(yàn)用到的全局變量 ts 不再需要了敢辩。
當(dāng)前進(jìn)程指針
每個(gè)CPU都要有自己運(yùn)行的當(dāng)前CPU運(yùn)行的當(dāng)前進(jìn)程(Env)的指針curenv蔽莱,存儲(chǔ)在 cpus[cpunum()].cpu_env
或thiscpu->cpu_env
。
系統(tǒng)寄存器
所有寄存器戚长,包括系統(tǒng)寄存器都是每個(gè)CPU獨(dú)有的盗冷。因此lcr3,ltr同廉,lgdt仪糖,lidt 這些指令在每個(gè)CPU上都要執(zhí)行一次,其中env_init_per_cpu()
和trap_init_per_cpu()
就是用于這個(gè)目的迫肖。
具體實(shí)現(xiàn)見作業(yè)3-4锅劝。
1.4 內(nèi)核鎖
在mp_main中初始化AP后,我們開始spin循環(huán)蟆湖。在AP進(jìn)一步操作前故爵,我們需要解決多個(gè)CPU同時(shí)運(yùn)行內(nèi)核代碼時(shí)的資源競(jìng)爭(zhēng)問(wèn)題,因?yàn)槎噙M(jìn)程同時(shí)運(yùn)行內(nèi)核代碼隅津,會(huì)影響內(nèi)核中的數(shù)據(jù)正確性诬垂。最簡(jiǎn)單的方式是使用big kernel lock
(大內(nèi)核鎖),進(jìn)程在進(jìn)入內(nèi)核時(shí)獲取大內(nèi)核鎖伦仍,回到用戶態(tài)時(shí)釋放鎖结窘。在該模式下,進(jìn)程可以并發(fā)的運(yùn)行在空閑的CPU上呢铆,但是同時(shí)只能有一個(gè)進(jìn)程運(yùn)行在內(nèi)核態(tài)晦鞋,其他進(jìn)程想進(jìn)入內(nèi)核態(tài)必須等待。
大內(nèi)核鎖在kern/spinlock.h
中定義棺克,可以通過(guò)lock_kernel()
和unlock_kernel()
來(lái)進(jìn)行加鎖和解鎖悠垛。我們需要在下面幾處位置加鎖和釋放鎖。
- 在
i386_init()
中娜谊,在BSP喚醒AP前加鎖确买。 - 在
mp_main()
中,初始化AP后加鎖纱皆,并調(diào)用sched_yield()
在該AP上運(yùn)行進(jìn)程湾趾。 - 在
trap()
中芭商,進(jìn)程從用戶態(tài)陷入時(shí)加鎖。 - 在
env_run()
中搀缠,進(jìn)程切換到用戶態(tài)之前釋放鎖铛楣。
這樣,我們?cè)贐SP啟動(dòng)AP前艺普,先加了鎖簸州。AP經(jīng)過(guò)mp_main()初始化后,因?yàn)榇藭r(shí)BSP持有鎖歧譬,所以AP的sched_yield()
需要等待岸浑,而當(dāng)BSP執(zhí)行調(diào)度運(yùn)行進(jìn)程后,會(huì)釋放鎖瑰步,此時(shí)等待鎖的AP便會(huì)獲取到鎖并執(zhí)行其他進(jìn)程矢洲。
1.5 輪轉(zhuǎn)調(diào)度
輪轉(zhuǎn)調(diào)度(round-robin)在sched_yield()
中完成,核心思想就是從進(jìn)程列表中找到一個(gè)狀態(tài)為 ENV_RUNNABLE 的進(jìn)程運(yùn)行缩焦。注意读虏,不能同時(shí)有兩個(gè)CPU運(yùn)行同一個(gè)進(jìn)程,這個(gè)可以根據(jù)進(jìn)程狀態(tài)進(jìn)行判斷袁滥,已經(jīng)運(yùn)行的進(jìn)程狀態(tài)為 ENV_RUNNING 掘譬。如果在列表中找不到ENV_RUNNABLE的進(jìn)程,而之前運(yùn)行的進(jìn)程又處于ENV_RUNNING狀態(tài)呻拌,則可以繼續(xù)運(yùn)行之前的進(jìn)程。
修改了kern/init.c
運(yùn)行3個(gè)user_yield
進(jìn)程睦焕,可以看到輸出如下:
# make qemu CPUS=2
Hello, I am environment 00001000.
Hello, I am environment 00001001.
Back in environment 00001000, iteration 0.
Hello, I am environment 00001002.
Back in environment 00001001, iteration 0.
Back in environment 00001000, iteration 1.
Back in environment 00001002, iteration 0.
Back in environment 00001001, iteration 1.
Back in environment 00001000, iteration 2.
Back in environment 00001002, iteration 1.
Back in environment 00001001, iteration 2.
Back in environment 00001000, iteration 3.
Back in environment 00001002, iteration 2.
Back in environment 00001001, iteration 3.
Back in environment 00001000, iteration 4.
Back in environment 00001002, iteration 3.
All done in environment 00001000.
[00001000] exiting gracefully
[00001000] free env 00001000
Back in environment 00001001, iteration 4.
Back in environment 00001002, iteration 4.
All done in environment 00001001.
All done in environment 00001002.
[00001001] exiting gracefully
[00001001] free env 00001001
[00001002] exiting gracefully
[00001002] free env 00001002
流程如下:
BSP先加載3個(gè)進(jìn)程藐握,并設(shè)置為ENV_RUNNBALE狀態(tài)。
BSP先喚醒AP垃喊,由于BSP先在i386_init時(shí)持有內(nèi)核鎖猾普,所以BSP先運(yùn)行進(jìn)程1 0x1000,運(yùn)行進(jìn)程時(shí) env_run() 切換到用戶態(tài)前會(huì)釋放內(nèi)核鎖本谜,此時(shí)等待鎖的AP開始運(yùn)行
sched_yield
初家,這樣 AP 會(huì)開始運(yùn)行進(jìn)程2 0x1001,開始運(yùn)行后釋放內(nèi)核鎖乌助。BSP打印出進(jìn)程號(hào)后調(diào)用了
sys_yield()
溜在,陷入到內(nèi)核的trap()里面會(huì)加內(nèi)核鎖,所以等到AP開始運(yùn)行進(jìn)程2且打印了進(jìn)程號(hào)后他托,BSP此時(shí)運(yùn)行進(jìn)程3掖肋。此后兩個(gè)CPU輪流調(diào)度可運(yùn)行的三個(gè)進(jìn)程。
1.6 創(chuàng)建進(jìn)程的系統(tǒng)調(diào)用
Unix提供了fork()系統(tǒng)調(diào)用創(chuàng)建進(jìn)程赏参,Unix拷貝了調(diào)用進(jìn)程(父進(jìn)程)的整個(gè)地址空間用于創(chuàng)建新進(jìn)程(子進(jìn)程)志笼,在用戶空間看來(lái)他們的唯一區(qū)別就是進(jìn)程ID不同沿盅。在父進(jìn)程中,fork()返回子進(jìn)程ID纫溃,而在子進(jìn)程中腰涧,fork()返回0。默認(rèn)情況下父子進(jìn)程都有自己的私有地址空間紊浩,且它們對(duì)內(nèi)存修改互不影響窖铡。
在JOS中我們要提供幾個(gè)不同的系統(tǒng)調(diào)用用于創(chuàng)建進(jìn)程,這也是Unix早期實(shí)現(xiàn)fork()的方式郎楼,下一節(jié)會(huì)討論使用 COW 技術(shù)實(shí)現(xiàn)的新的fork()万伤。
sys_exofork
這個(gè)系統(tǒng)調(diào)用創(chuàng)建了一個(gè)幾乎空白的新的進(jìn)程,它沒有任何東西映射到其地址空間的用戶部分呜袁,且它不可運(yùn)行敌买。這個(gè)新的進(jìn)程與父進(jìn)程有一樣的寄存器狀態(tài),在父進(jìn)程中阶界,它返回子進(jìn)程的envid虹钮,而在子進(jìn)程中,它返回0膘融。由于sys_exofork初始化將子進(jìn)程標(biāo)記為ENV_NOT_RUNNABLE芙粱,因此sys_exofork不會(huì)返回到子進(jìn)程,只有父進(jìn)程用sys_env_set_status將其狀態(tài)設(shè)置 ENV_RUNNABLE 后氧映,子進(jìn)程才能運(yùn)行春畔。
sys_env_set_status
設(shè)置指定的進(jìn)程狀態(tài)為 ENV_NOT_RUNNABLE 或者 ENV_RUNNABLE,用于標(biāo)記進(jìn)程可以開始運(yùn)行岛都。
sys_page_alloc
用于分配一頁(yè)物理內(nèi)存并將其映射到指定的虛擬地址律姨。不同于page_alloc,sys_page_alloc不僅分配了物理頁(yè)臼疫,而且要通過(guò)page_insert()將分配的物理頁(yè)映射到虛擬地址va择份。
sys_page_map
從一個(gè)進(jìn)程的地址空間拷貝一個(gè)頁(yè)面映射(注意,不是拷貝頁(yè)的內(nèi)容)到另一個(gè)進(jìn)程的地址空間烫堤。其實(shí)就是用于將父進(jìn)程的某個(gè)臨時(shí)地址空間如UTEMP映射到子進(jìn)程的新分配的物理頁(yè)荣赶,方便父進(jìn)程訪問(wèn)子進(jìn)程新分配的內(nèi)存以拷貝數(shù)據(jù)。
sys_page_unmap
取消指定進(jìn)程的指定虛擬地址處的頁(yè)面映射以下次重復(fù)使用鸽斟。
所有上面的系統(tǒng)調(diào)用都接收進(jìn)程ID參數(shù)拔创,如果傳0表示指當(dāng)前進(jìn)程。通過(guò)進(jìn)程ID得到進(jìn)程env對(duì)象可以通過(guò)函數(shù) kern/env.c
中的 envidenv() 實(shí)現(xiàn)富蓄。
在 user/dumbfork.c中有一個(gè)類似unix的fork()的實(shí)現(xiàn)伏蚊,它使用了上面這幾個(gè)系統(tǒng)調(diào)用運(yùn)行了子進(jìn)程,子進(jìn)程拷貝了父進(jìn)程的地址空間格粪。父子進(jìn)程交替切換躏吊,最后父進(jìn)程在循環(huán)10次后退出氛改,而子進(jìn)程則是循環(huán)20次后退出。
void
duppage(envid_t dstenv, void *addr)
{
int r;
// This is NOT what you should do in your fork.
if ((r = sys_page_alloc(dstenv, addr, PTE_P|PTE_U|PTE_W)) < 0)
panic("sys_page_alloc: %e", r);
if ((r = sys_page_map(dstenv, addr, 0, UTEMP, PTE_P|PTE_U|PTE_W)) < 0)
panic("sys_page_map: %e", r);
memmove(UTEMP, addr, PGSIZE);
if ((r = sys_page_unmap(0, UTEMP)) < 0)
panic("sys_page_unmap: %e", r);
}
user/dumbfork.c 中的dumbfork()具體實(shí)現(xiàn)流程是這樣的:
- 1)先通過(guò) sys_exofork() 系統(tǒng)調(diào)用創(chuàng)建一個(gè)新的空白進(jìn)程比伏。
- 2)然后通過(guò)duppage拷貝父進(jìn)程的地址空間到子進(jìn)程中胜卤。用戶進(jìn)程地址空間開始位置是UTEXT(0x00800000) ,結(jié)束位置是 end赁项。duppage是一頁(yè)頁(yè)拷貝的葛躏,它將父進(jìn)程的addr開始的一頁(yè)物理內(nèi)存內(nèi)容拷貝到子進(jìn)程dstenv的對(duì)應(yīng)的頁(yè)中。
-
- 完成父進(jìn)程到子進(jìn)程內(nèi)存數(shù)據(jù)的拷貝悠菜。
- 3.1)先通過(guò)sys_page_alloc為子進(jìn)程addr開始的一頁(yè)內(nèi)容分配一個(gè)物理頁(yè)并完成映射舰攒,此時(shí),分配的物理頁(yè)還是空的悔醋,沒有數(shù)據(jù)摩窃。然后通過(guò) sys_page_map 將子進(jìn)程va開始的這分配好的物理頁(yè)映射到父進(jìn)程的UTEMP地址處(0x00400000),這么做的目的就是為了在父進(jìn)程中訪問(wèn)到子進(jìn)程新分配的物理頁(yè)芬骄。
- 3.2)接下來(lái)猾愿,通過(guò)memmove函數(shù)將父進(jìn)程addr處的一頁(yè)數(shù)據(jù)拷貝到了UTEMP中,而因?yàn)榍懊婵吹経TEMP已經(jīng)映射到了子進(jìn)程的那頁(yè)內(nèi)存账阻,所以最終效果就是將父進(jìn)程的addr處的一頁(yè)內(nèi)存數(shù)據(jù)拷貝到子進(jìn)程的addr對(duì)應(yīng)的那頁(yè)內(nèi)存完成數(shù)據(jù)的復(fù)制蒂秘。
- 3.3)最后通過(guò) sys_page_unmap 取消父進(jìn)程在UTEMP的映射以下次使用,當(dāng)然還有個(gè)重要目的是預(yù)防父進(jìn)程誤操作到子進(jìn)程的內(nèi)存數(shù)據(jù)淘太。
2 寫時(shí)復(fù)制(Copy On Write)
前面實(shí)現(xiàn)fork是直接將父進(jìn)程的數(shù)據(jù)拷貝到了子進(jìn)程姻僧,這是Unix系統(tǒng)最初采用的方式,但是這樣有個(gè)問(wèn)題就是會(huì)造成資源浪費(fèi)蒲牧,很多時(shí)候我們fork一個(gè)子進(jìn)程段化,接著是直接exec替換子進(jìn)程的內(nèi)存直接執(zhí)行另一個(gè)程序,子進(jìn)程在exec之前用到父進(jìn)程的內(nèi)存數(shù)據(jù)很少造成。
于是后續(xù)的Unix版本優(yōu)化了fork,利用了虛擬內(nèi)存硬件支持的方式雄嚣,fork時(shí)拷貝的是地址空間而不是物理內(nèi)存數(shù)據(jù)晒屎,這樣,父子進(jìn)程各自的地址空間都映射到同樣的內(nèi)存數(shù)據(jù)缓升,共享的內(nèi)存頁(yè)會(huì)被標(biāo)記為只讀鼓鲁。當(dāng)父子進(jìn)程有一方要修改共享內(nèi)存時(shí),此時(shí)會(huì)報(bào)page fault
錯(cuò)誤港谊,此時(shí)Unix內(nèi)核會(huì)為報(bào)錯(cuò)的進(jìn)程分配一個(gè)新的物理頁(yè)骇吭,并拷共享內(nèi)存頁(yè)的數(shù)據(jù)到新分配的物理頁(yè)中。執(zhí)行exec時(shí)歧寺,只需要拷貝堆棧這一個(gè)頁(yè)面即可燥狰。
2.1 用戶程序頁(yè)面錯(cuò)誤處理
為了實(shí)現(xiàn)寫時(shí)復(fù)制棘脐,首先要實(shí)現(xiàn)用戶程序頁(yè)面錯(cuò)誤處理功能×拢基本流程是:
- 1)用戶進(jìn)程通過(guò) set_pgfault_handler(handler) 設(shè)置頁(yè)面錯(cuò)誤處理函數(shù)蛀缝。
- 2)函數(shù)set_pgfault_handler中為用戶程序分配異常棧,通過(guò)系統(tǒng)調(diào)用sys_env_set_pgfault_upcall 設(shè)置通用的頁(yè)面錯(cuò)誤處理調(diào)用入口目代。
- 3)當(dāng)用戶進(jìn)程發(fā)生頁(yè)面錯(cuò)誤時(shí)屈梁,陷入內(nèi)核。內(nèi)核先判斷該進(jìn)程是否設(shè)置了 env_pgfault_upcall榛了,如果沒有設(shè)置在讶,則報(bào)錯(cuò)。如果設(shè)置了霜大,則切換用戶進(jìn)程棧到異常棧构哺,設(shè)置異常棧內(nèi)容,然后設(shè)置EIP為 env_pgfault_upcall 地址僧诚,切回用戶態(tài)執(zhí)行 env_pgfault_upcall 函數(shù)(即_pgfault_upcall)遮婶。
- 4)env_pgfault_upcall作為頁(yè)面錯(cuò)誤處理函數(shù)的入口函數(shù),它在用戶態(tài)運(yùn)行湖笨。先調(diào)用步驟1中注冊(cè)的頁(yè)面錯(cuò)誤處理函數(shù)旗扑,然后再恢復(fù)進(jìn)程在頁(yè)面錯(cuò)誤之前的棧內(nèi)容,并切回常規(guī)棧慈省,跳轉(zhuǎn)到頁(yè)面錯(cuò)誤之前的地方繼續(xù)運(yùn)行臀防。
設(shè)置用戶級(jí)頁(yè)面錯(cuò)誤處理函數(shù)
前面提到,新的fork并不直接拷貝內(nèi)存數(shù)據(jù)边败,而是先對(duì)共享的內(nèi)存頁(yè)設(shè)置一個(gè)特殊標(biāo)記袱衷,然后在父子進(jìn)程的一方寫共享內(nèi)存發(fā)生頁(yè)面錯(cuò)誤時(shí),內(nèi)核捕獲異常并分配新的頁(yè)和拷貝數(shù)據(jù)笑窜。這里首先要實(shí)現(xiàn)的是對(duì)用戶級(jí)的頁(yè)面錯(cuò)誤的捕獲和處理致燥。
COW只是用戶級(jí)頁(yè)面錯(cuò)誤處理的許多可能用途之一。大多數(shù)Unix內(nèi)核最初只映射新進(jìn)程的堆棧排截,隨著堆棧消耗增加嫌蚤,訪問(wèn)尚未映射的堆棧地址會(huì)導(dǎo)致頁(yè)面錯(cuò)誤,內(nèi)核捕獲錯(cuò)誤后會(huì)分配并映射附加的堆棧頁(yè)面断傲。典型的Unix內(nèi)核必須跟蹤進(jìn)程空間的每個(gè)區(qū)域發(fā)生頁(yè)面錯(cuò)誤時(shí)要采取的操作脱吱。例如,堆棧區(qū)域中的錯(cuò)誤通常會(huì)分配并映射新的物理內(nèi)存頁(yè)面认罩,程序的BSS區(qū)域中的錯(cuò)誤通常會(huì)分配一個(gè)新頁(yè)面箱蝠,填充零并映射它。而可執(zhí)行代碼中導(dǎo)致的頁(yè)面錯(cuò)誤將觸發(fā)內(nèi)核從磁盤讀取可執(zhí)行文件的相應(yīng)頁(yè)面,然后映射它宦搬。
為了處理用戶進(jìn)程頁(yè)面錯(cuò)誤牙瓢,用戶進(jìn)程需要設(shè)置一個(gè)頁(yè)面錯(cuò)誤處理函數(shù),新增加一個(gè)系統(tǒng)調(diào)用sys_env_set_pgfault_call
來(lái)設(shè)置Env結(jié)構(gòu)體的 env_pgfault_upcall 字段即可床三。
用戶進(jìn)程異常棧和常規(guī)棧
而為了處理用戶級(jí)頁(yè)面錯(cuò)誤一罩,JOS采用了一個(gè)用戶異常棧UXSTACKTOP(0xeec00000),注意用戶進(jìn)程的常規(guī)棧用的是USTACKTOP(0xeebfe000)撇簿。當(dāng)用戶進(jìn)程發(fā)生頁(yè)面錯(cuò)誤時(shí)聂渊,內(nèi)核會(huì)切換到異常棧,異常棧大小也是PGSIZE四瘫。從用戶常規(guī)棧切換到異常棧的過(guò)程有點(diǎn)像發(fā)生中斷/異常時(shí)從用戶態(tài)進(jìn)入內(nèi)核時(shí)的堆棧切換汉嗽。
當(dāng)運(yùn)行在異常棧時(shí),用戶級(jí)頁(yè)面錯(cuò)誤處理函數(shù)可以調(diào)用JOS的常規(guī)系統(tǒng)調(diào)用去映射新的頁(yè)面找蜜,以期修復(fù)導(dǎo)致頁(yè)面錯(cuò)誤的問(wèn)題饼暑。當(dāng)用戶級(jí)頁(yè)面錯(cuò)誤處理函數(shù)處理完成后,再通過(guò)一段匯編代碼返回到常規(guī)堆棧存儲(chǔ)的發(fā)生頁(yè)面錯(cuò)誤的地址處繼續(xù)運(yùn)行洗做。
需要支持用戶級(jí)頁(yè)面錯(cuò)誤處理的用戶進(jìn)程都需要為它的異常棧分配內(nèi)存弓叛,可以使用前面用過(guò)的sys_page_alloc分配內(nèi)存。
調(diào)用用戶頁(yè)面錯(cuò)誤處理函數(shù)
修改kern/trap.c
中的頁(yè)面錯(cuò)誤處理代碼以支持用戶進(jìn)程的頁(yè)面錯(cuò)誤處理诚纸。如果用戶進(jìn)程沒有注冊(cè)頁(yè)面錯(cuò)誤處理函數(shù)撰筷,則跟之前一樣返回錯(cuò)誤即可。而如果設(shè)置了頁(yè)面錯(cuò)誤處理函數(shù)畦徘,則需要在異常棧中壓入下面內(nèi)容以記錄出錯(cuò)狀態(tài)毕籽,這些內(nèi)容正好構(gòu)成了一個(gè)UTrapframe結(jié)構(gòu)體,方便統(tǒng)一處理井辆,接著設(shè)置EIP為env_pgfault_upcall函數(shù)地址关筒,并將進(jìn)程的堆棧切換到異常棧,然后開始運(yùn)行頁(yè)面錯(cuò)誤處理函數(shù)杯缺。
頁(yè)面錯(cuò)誤處理函數(shù)是在lib/pfentry.S
中定義的蒸播,它首先要執(zhí)行用戶程序中定義的pgfault_handler函數(shù),然后再回到程序出錯(cuò)位置繼續(xù)運(yùn)行萍肆。
需要注意的是袍榆,如果用戶進(jìn)程已經(jīng)運(yùn)行在異常棧了,此時(shí)又發(fā)生嵌套頁(yè)面錯(cuò)誤匾鸥,則需要在tf->tf_esp
而不是從UXSTACKTOP壓入異常數(shù)據(jù),而且這種情況下碉纳,你要保留一個(gè)空的4字節(jié)勿负,再壓入U(xiǎn)Trapframe。要檢查用戶進(jìn)程是否運(yùn)行在異常棧,可以檢查 tf->tf_esp 是否在區(qū)間 [UXSTACKTOP-PGSIZE, UXSTACKTOP-1]奴愉。
<-- UXSTACKTOP
trap-time esp // 頁(yè)面錯(cuò)誤時(shí)用戶棧的地址
trap-time eflags
trap-time eip
trap-time eax start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi end of struct PushRegs
tf_err (error code)
fault_va <-- %esp when handler is run
回到頁(yè)面錯(cuò)誤處繼續(xù)執(zhí)行
在執(zhí)行完頁(yè)面錯(cuò)誤處理函數(shù)后琅摩,需要回到用戶進(jìn)程之前出錯(cuò)的位置繼續(xù)執(zhí)行,這里需要完成 lib/pfentry.S
中的_pgfault_upcall
函數(shù)锭硼。
這個(gè)函數(shù)做的工作就是將異常棧切換到常規(guī)棧,重新設(shè)置 EIP,注意之前頁(yè)面出錯(cuò)地址存儲(chǔ)在 fault_va 中觉吭。這里要添加代碼如下坎怪,此時(shí)esp指向的是前一節(jié)的UTrapframe的地址,這里做的工作主要是:
- 將用戶進(jìn)程的常規(guī)棧當(dāng)前位置減去4字節(jié)暑始,然后將用戶進(jìn)程頁(yè)面錯(cuò)誤時(shí)的EIP存儲(chǔ)到該位置搭独。這樣恢復(fù)常規(guī)棧的時(shí)候,棧頂存儲(chǔ)的是出錯(cuò)時(shí)的EIP廊镜。
- 然后將異常棧中存儲(chǔ)的用戶進(jìn)程頁(yè)面錯(cuò)誤時(shí)的通用寄存器和eflags寄存器的值還原牙肝。
- 然后將異常棧中存儲(chǔ)的esp的值還原到esp寄存器。
- 最后通過(guò)ret指令返回到用戶進(jìn)程出錯(cuò)時(shí)的地址繼續(xù)執(zhí)行嗤朴。(ret指令執(zhí)行的操作就是將彈出棧頂元素配椭,并將EIP設(shè)置為該值,此時(shí)正好棧頂是我們?cè)谥霸O(shè)置的出錯(cuò)時(shí)的EIP的值)
- 現(xiàn)在可以看到如果發(fā)生嵌套頁(yè)錯(cuò)誤為什么多保留4個(gè)字節(jié)了雹姊,這是因?yàn)榘l(fā)生嵌套頁(yè)錯(cuò)誤時(shí)股缸,此時(shí)我們的trap-time esp存儲(chǔ)的是異常棧,此時(shí)會(huì)將trap-time的EIP的值會(huì)被設(shè)置到esp-4處容为,如果不空出4字節(jié)乓序,則會(huì)覆蓋原來(lái)的esp值了。
movl 0x28(%esp), %ebx # trap-time時(shí)的eip坎背,注意UTrapframe結(jié)構(gòu)
subl $0x4, 0x30(%esp)
movl 0x30(%esp), %eax
movl %ebx, (%eax) # 將trap-time的eip拷貝到trap-time esp-4處
addl $0x8, %esp
popal
addl $0x4, %esp # 設(shè)置eflags
popfl
popl %esp # 將棧頂?shù)刂窂棾龅絜sp替劈,此時(shí)棧頂值是用戶進(jìn)程出錯(cuò)時(shí)的eip值
ret
最后還要完成lib/pgfault.c
中的set_pgfault_handler
函數(shù),用于為用戶進(jìn)程分配異常棧以及頁(yè)面錯(cuò)誤處理函數(shù) env_pgfault_upcall 的初始化設(shè)置得滤。
2.2 實(shí)現(xiàn)寫時(shí)復(fù)制fork
完成上一節(jié)準(zhǔn)備工作后陨献,開始實(shí)現(xiàn)COW的fork,fork實(shí)現(xiàn)的流程如下:
- 1)父進(jìn)程設(shè)置pgfault()函數(shù)為頁(yè)面錯(cuò)誤處理函數(shù)懂更,用到前面的 set_pgfault_handler 函數(shù)眨业。
- 2)父進(jìn)程調(diào)用 sys_exofork() 創(chuàng)建一個(gè)空白子進(jìn)程。
- 3)對(duì)父進(jìn)程在UTOP之下的可寫或者COW的物理頁(yè)沮协,父進(jìn)程調(diào)用duppage龄捡,duppage會(huì)將這些頁(yè)面設(shè)置為COW映射到子進(jìn)程的地址空間,同時(shí)慷暂,也要將父進(jìn)程本身的頁(yè)面重新映射聘殖,將頁(yè)面權(quán)限設(shè)置為COW(注:子進(jìn)程的COW設(shè)置要在父進(jìn)程之前)。duppage將父子進(jìn)程相關(guān)頁(yè)面權(quán)限設(shè)置為不可寫,且在avail字段設(shè)置為COW奸腺,用于區(qū)分只讀頁(yè)面和COW頁(yè)面餐禁。異常棧不以這種方式重新映射,需要在子進(jìn)程分配一個(gè)新的頁(yè)面給異常棧用突照。fork()還要處理那些不是可寫的且不是COW的頁(yè)面帮非。
- 4)父進(jìn)程設(shè)置子進(jìn)程的頁(yè)面錯(cuò)誤處理函數(shù)。
- 5)父進(jìn)程標(biāo)識(shí)子進(jìn)程狀態(tài)為可運(yùn)行讹蘑。
當(dāng)父子進(jìn)程中任意一個(gè)試圖修改一個(gè)還沒有寫過(guò)的COW頁(yè)面末盔,會(huì)觸發(fā)頁(yè)面錯(cuò)誤,開始下面流程:
- 1)內(nèi)核發(fā)現(xiàn)用戶程序頁(yè)面錯(cuò)誤后衔肢,轉(zhuǎn)至_pgfault_upcall處理庄岖,而_pgfault_upcall會(huì)調(diào)用pgfault()。
- 2)pgfault()檢查這是一個(gè)寫錯(cuò)誤(錯(cuò)誤碼中的FEC_WR)且頁(yè)面權(quán)限是COW的角骤,如果不是則報(bào)錯(cuò)隅忿。
- 3)pgfault()分配一個(gè)新的物理頁(yè),并映射到一個(gè)臨時(shí)位置邦尊,然后將出錯(cuò)頁(yè)面的內(nèi)容拷貝到新的物理頁(yè)中背桐,然后將新的頁(yè)設(shè)置為用戶可讀寫權(quán)限,并映射到對(duì)應(yīng)位置蝉揍。
fork()链峭,pgfault(),duppage()三個(gè)函數(shù)的具體實(shí)現(xiàn)見作業(yè)12。完成后make run-forktree
又沾,正常應(yīng)該輸出下面的內(nèi)容(順序可能不同):
1000: I am ''
1001: I am '0'
2000: I am '00'
2001: I am '000'
1002: I am '1'
3000: I am '11'
3001: I am '10'
4000: I am '100'
1003: I am '01'
5000: I am '010'
4001: I am '011'
2002: I am '110'
1004: I am '001'
1005: I am '111'
1006: I am '101'
forktree這個(gè)程序比較有意思弊仪,它先創(chuàng)建兩個(gè)子進(jìn)程打印第一層 0, 1杖刷,然后子進(jìn)程再分別創(chuàng)建子進(jìn)程打印一棵樹出來(lái)励饵,比如兩層是這樣的,打印結(jié)果是 '', 0, 1, 00, 01, 10, 11
滑燃。
‘’
/ \
0 1
/\ /\
0 1 0 1
3 搶占式調(diào)度和進(jìn)程間通信
3.1 時(shí)鐘中斷
最后一部分是通過(guò)時(shí)鐘中斷來(lái)完成搶占式調(diào)度役听。運(yùn)行make run-spin
可以看到子進(jìn)程死循環(huán)占用了CPU,沒法切換到其他進(jìn)程了表窘,現(xiàn)在需要通過(guò)時(shí)鐘中斷來(lái)強(qiáng)制調(diào)度典予。時(shí)鐘中斷屬于可屏蔽中斷,可以通過(guò) eflags 寄存器的IF位來(lái)控制乐严,注意由int指令觸發(fā)的軟件中斷不受eflags寄存器的控制瘤袖,它是不可屏蔽中斷,此外NMI也屬于不可屏蔽中斷昂验。
外部中斷通常稱之為 IRQ捂敌,IRQ到中斷描述符表的入口不是固定的昭娩。不過(guò)在 pic_init 中我們將IRQ的0-15映射到了IDT的[IRQ_OFFSET, IRQ_OFFSET+15]。其中IRQ_OFFSET為32黍匾,所以IRQ在IDT中范圍為[32, 47],共16個(gè)呛梆。JOS中對(duì)中斷做了簡(jiǎn)化處理锐涯,在內(nèi)核態(tài)時(shí)外部中斷是禁止的,在用戶態(tài)時(shí)才會(huì)開啟填物。中斷開啟和禁止是通過(guò)eflags寄存器的 FL_IF 位來(lái)控制纹腌,為1表示開啟中斷,為0則禁止中斷滞磺。
接下來(lái)類似實(shí)驗(yàn)3那樣升薯,設(shè)置中斷號(hào)和中斷處理程序。注意在實(shí)驗(yàn)3中我將istrap基本都設(shè)置為1了击困,雖然那時(shí)候不影響實(shí)驗(yàn)結(jié)果涎劈,在實(shí)驗(yàn)4這里必須要全部將istrap值設(shè)為0。因?yàn)镴OS中的這個(gè)istrap設(shè)為1就會(huì)在開始處理中斷時(shí)將FL_IF置為1阅茶,而設(shè)為0則保持FL_IF不變蛛枚,設(shè)為0才能通過(guò)trap()中對(duì)FL_IF的檢查。最后在 trap() 函數(shù)中處理 IRQ_TIMER中斷脸哀,調(diào)用lapic_eio()
和sched_yield()
即可蹦浦。
3.2 進(jìn)程間通信(IPC)
最后要完成進(jìn)程間通信,常見的一個(gè)IPC例子就是管道撞蜂。實(shí)現(xiàn)IPC有很多方式盲镶,哪種方式最好至今仍有爭(zhēng)論,JOS中會(huì)實(shí)現(xiàn)一種簡(jiǎn)單的IPC機(jī)制蝌诡。需要完成 sys_ipc_try_send() 和 sys_ipc_recv() 兩個(gè)系統(tǒng)調(diào)用溉贿,以及封裝了這兩個(gè)系統(tǒng)調(diào)用的庫(kù)函數(shù)實(shí)現(xiàn)。
JOS IPC中的消息包括兩個(gè)部分:一個(gè)32位的值以及一個(gè)可選的頁(yè)面映射送漠。消息中包含這個(gè)頁(yè)面映射是為了傳輸更多的數(shù)據(jù)以及實(shí)現(xiàn)進(jìn)程間共享內(nèi)存顽照。
進(jìn)程調(diào)用 sys_ipc_recv() 接收消息,調(diào)用 sys_ipc_try_send() 發(fā)送消息闽寡。如果要發(fā)送頁(yè)面映射代兵,則調(diào)用時(shí)設(shè)置srcva參數(shù),表示要將srcva處的頁(yè)面映射共享給接收進(jìn)程爷狈。而接收進(jìn)程的 sys_ipc_try_recv() 如果希望接收頁(yè)面映射植影,則會(huì)提供一個(gè) dstva 參數(shù)。如果發(fā)送進(jìn)程和接收進(jìn)程都沒有設(shè)置參數(shù)表示希望傳輸頁(yè)面映射涎永,則不傳輸思币。內(nèi)核會(huì)在接收進(jìn)程的 env_ipc_perm字段設(shè)置接收的頁(yè)面映射的權(quán)限鹿响。
任何進(jìn)程都可以發(fā)送消息給其他進(jìn)程,不需要它們是父子進(jìn)程谷饿。這里的安全由IPC相關(guān)系統(tǒng)調(diào)用保障惶我,一個(gè)進(jìn)程不能通過(guò)發(fā)送消息導(dǎo)致另一個(gè)進(jìn)程奔潰,除非接收消息的進(jìn)程本身存在BUG博投。
4 一些注意點(diǎn)
完成作業(yè)15后绸贡,可以發(fā)現(xiàn)stresssched通不過(guò)測(cè)試,這個(gè)有個(gè)坑毅哗,檢查了很久才發(fā)現(xiàn)听怕,原來(lái)要在
kern/sched.c
的sched_halt(void)
中去掉//sti
的注釋,因?yàn)樵贏P啟動(dòng)完成且獲得鎖且第一次調(diào)用 sched_yield()時(shí)虑绵,如果發(fā)現(xiàn)沒有可運(yùn)行進(jìn)程尿瞭,會(huì)執(zhí)行sched_halt()導(dǎo)致CPU處于HALT狀態(tài)。因?yàn)槲覀冊(cè)赽ootloader中通過(guò)cli關(guān)閉了中斷的翅睛,所以此時(shí)需要開啟中斷声搁,不然AP就一直處于HALT狀態(tài)而不參與調(diào)度了。另外捕发,spin測(cè)試不要多加參數(shù)如
CPUS=2
酥艳,否則會(huì)測(cè)試失敗,因?yàn)楫?dāng)父子進(jìn)程在不同的CPU運(yùn)行時(shí)爬骤,此時(shí)父進(jìn)程去銷毀子進(jìn)程會(huì)先將子進(jìn)程設(shè)置為 ENV_DYING 狀態(tài)充石,而后等子進(jìn)程調(diào)度的時(shí)候再自己銷毀自己,這會(huì)跟要求輸出不一樣導(dǎo)致通不過(guò)測(cè)試霞玄。一些調(diào)試語(yǔ)句要注意輸出位置骤铃,可能會(huì)干擾測(cè)試結(jié)果,因?yàn)樽鳂I(yè)是根據(jù)輸出來(lái)判定的坷剧,最好去掉多余的調(diào)試語(yǔ)句來(lái)測(cè)試惰爬。