Interrupt硬件部分
中斷對(duì)應(yīng)的場(chǎng)景很簡(jiǎn)單骄噪,就是硬件想要得到操作系統(tǒng)的關(guān)注莽鸿。例如網(wǎng)卡收到了一個(gè)packet山憨,網(wǎng)卡會(huì)生成一個(gè)中斷焦蘑;用戶通過(guò)鍵盤(pán)按下了一個(gè)按鍵盯拱,鍵盤(pán)會(huì)產(chǎn)生一個(gè)中斷.
3個(gè)差別: 異步,Interrupt handler與當(dāng)前運(yùn)行的進(jìn)程在CPU上沒(méi)有任何關(guān)聯(lián); 并行,設(shè)備handler 和 CPU 同時(shí)運(yùn)行. 設(shè)備編程,每個(gè)設(shè)備都有一個(gè)編程手冊(cè).
從左上角可以看出,我們有53個(gè)不同的來(lái)自于設(shè)備的中斷例嘱。這些中斷到達(dá)PLIC之后狡逢,PLIC會(huì)路由這些中斷。圖的右下角是CPU的核拼卵,PLIC會(huì)將中斷路由到某一個(gè)CPU的核.
- PLIC會(huì)通知當(dāng)前有一個(gè)待處理的中斷
- 其中一個(gè)CPU核會(huì)Claim接收中斷奢浑,這樣PLIC就不會(huì)把中斷發(fā)給其他的CPU處理
- CPU核處理完中斷之后,CPU會(huì)通知PLIC
- PLIC將不再保存中斷的信息
設(shè)備驅(qū)動(dòng)概述
管理設(shè)備的代碼稱為驅(qū)動(dòng).
UART設(shè)備的驅(qū)動(dòng)腋腮,代碼在uart.c文件中.如果我們查看代碼的結(jié)構(gòu)殷费,我們可以發(fā)現(xiàn)大部分驅(qū)動(dòng)都分為兩個(gè)部分,bottom/top低葫。
bottom
bottom部分通常是Interrupt handler.nterrupt handler并不運(yùn)行在任何特定進(jìn)程的context中详羡,它只是處理中斷
// handle a uart interrupt, raised because input has
// arrived, or the uart is ready for more output, or
// both. called from devintr().
void
uartintr(void)
{
// read and process incoming characters.
while(1){
int c = uartgetc();
if(c == -1)
break;
consoleintr(c);
}
// send buffered characters.
acquire(&uart_tx_lock);
uartstart();
release(&uart_tx_lock);
}
top
top部分,是用戶進(jìn)程嘿悬,或者內(nèi)核的其他部分去調(diào)用的接口
比如:
// alternate version of uartputc() that doesn't
// use interrupts, for use by kernel printf() and
// to echo characters. it spins waiting for the uart's
// output register to be empty.
void
uartputc_sync(int c)
{
push_off();
if(panicked){
for(;;)
;
}
// wait for Transmit Holding Empty to be set in LSR.
while((ReadReg(LSR) & LSR_TX_IDLE) == 0)
;
WriteReg(THR, c);
pop_off();
}
通常情況下实柠,驅(qū)動(dòng)中會(huì)有一些隊(duì)列(或者說(shuō)buffer),top部分的代碼會(huì)從隊(duì)列中讀寫(xiě)數(shù)據(jù)善涨,而Interrupt handler(bottom部分)同時(shí)也會(huì)向隊(duì)列中讀寫(xiě)數(shù)據(jù).
Interrupt handler來(lái)說(shuō)存在一些限制窒盐,因?yàn)樗](méi)有運(yùn)行在任何進(jìn)程的context中,所以進(jìn)程的page table并不知道該從哪個(gè)地址讀寫(xiě)數(shù)據(jù)钢拧,也就無(wú)法直接從Interrupt handler讀寫(xiě)數(shù)據(jù).
如何對(duì)設(shè)備進(jìn)行編程
操作系統(tǒng)需要知道這些設(shè)備位于物理地址空間的具體位置蟹漓,然后再通過(guò)普通的load/store指令對(duì)這些地址進(jìn)行編程。load/store指令實(shí)際上的工作就是讀寫(xiě)設(shè)備的控制寄存器源内。
例如葡粒,對(duì)網(wǎng)卡執(zhí)行store指令時(shí),CPU會(huì)修改網(wǎng)卡的某個(gè)控制寄存器,進(jìn)而導(dǎo)致網(wǎng)卡發(fā)送一個(gè)packet嗽交。所以這里的load/store指令不會(huì)讀寫(xiě)內(nèi)存卿嘲,而是會(huì)操作設(shè)備
Console是如何顯示出$ ls
$ 是Shell程序的輸出,而“l(fā)s”是用戶通過(guò)鍵盤(pán)輸入之后再顯示出來(lái)的.
實(shí)際上就是設(shè)備會(huì)將字符傳輸給UART的寄存器夫壁,UART之后會(huì)在發(fā)送完字符之后產(chǎn)生一個(gè)中斷.線路的另一端會(huì)有另一個(gè)UART芯片(模擬的)拾枣,這個(gè)UART芯片連接到了虛擬的Console,它會(huì)進(jìn)一步將$ 顯示在console上.
對(duì)于“l(fā)s”,當(dāng)你在鍵盤(pán)上按下一個(gè)按鍵盒让,UART芯片會(huì)將按鍵字符通過(guò)串口線發(fā)送到另一端的UART芯片.另一端的UART芯片先將數(shù)據(jù)bit合并成一個(gè)Byte梅肤,之后再產(chǎn)生一個(gè)中斷,并告訴處理器說(shuō)這里有一個(gè)來(lái)自于鍵盤(pán)的字符.
- SIE(Supervisor Interrupt Enable)寄存器邑茄。這個(gè)寄存器中有一個(gè)bit(E)專門(mén)針對(duì)例如UART的外部設(shè)備的中斷.
- SSTATUS(Supervisor Status)寄存器姨蝴。這個(gè)寄存器中有一個(gè)bit來(lái)打開(kāi)或者關(guān)閉中斷。每一個(gè)CPU核都有獨(dú)立的SIE和SSTATUS寄存器
- SIP(Supervisor Interrupt Pending)寄存器撩扒。當(dāng)發(fā)生中斷時(shí)似扔,處理器可以通過(guò)查看這個(gè)寄存器知道當(dāng)前是什么類型的中斷吨些。
我們第一個(gè)外設(shè)是console搓谆,這是我們print的輸出位置。查看位于console.c的consoleinit函數(shù)度液。
uartinit函數(shù)位于uart.c文件拥峦。這個(gè)函數(shù)實(shí)際上就是配置好UART芯片使其可以被使用
運(yùn)行完這個(gè)函數(shù)之后笔呀,原則上UART就可以生成中斷了。但是因?yàn)槲覀冞€沒(méi)有對(duì)PLIC編程斩萌,所以中斷不能被CPU感知。最終屏轰,在main函數(shù)中颊郎,需要調(diào)用plicinit函數(shù)。下圖是plicinit函數(shù)霎苗。
void
plicinit(void)
{
// set desired IRQ priorities non-zero (otherwise disabled).
*(uint32*)(PLIC + UART0_IRQ*4) = 1;
*(uint32*)(PLIC + VIRTIO0_IRQ*4) = 1;
}
代碼的第一行使能響應(yīng)UART的中斷姆吭,這里實(shí)際上就是設(shè)置PLIC會(huì)接收哪些中斷,進(jìn)而將中斷路由到CPU唁盏。
代碼的第二行設(shè)置PLIC接收來(lái)自IO磁盤(pán)的中斷内狸。
plicinit之后就是plicinithart函數(shù)。plicinit是由0號(hào)CPU運(yùn)行厘擂,之后昆淡,每個(gè)CPU的核都需要調(diào)用plicinithart函數(shù)表明對(duì)于哪些外設(shè)中斷感興趣。
void
plicinithart(void)
{
int hart = cpuid();
// set enable bits for this hart's S-mode
// for the uart and virtio disk.
*(uint32*)PLIC_SENABLE(hart) = (1 << UART0_IRQ) | (1 << VIRTIO0_IRQ);
// set this hart's S-mode priority threshold to 0.
*(uint32*)PLIC_SPRIORITY(hart) = 0;
}
在plicinithart函數(shù)中刽严,每個(gè)CPU的核都表明自己對(duì)來(lái)自于UART和VIRTIO的中斷感興趣昂灵。
到目前為止,我們有了生成中斷的外部設(shè)備,我們有了PLIC可以傳遞中斷到單個(gè)的CPU倔既。但是CPU自己還沒(méi)有設(shè)置好接收中斷恕曲,因?yàn)槲覀冞€沒(méi)有設(shè)置好SSTATUS寄存器.
在實(shí)際運(yùn)行進(jìn)程之前,會(huì)執(zhí)行intr_on函數(shù)來(lái)使得CPU能接收中斷渤涌。intr_on函數(shù)只完成一件事情佩谣,就是設(shè)置SSTATUS寄存器,打開(kāi)中斷標(biāo)志位实蓬。
// enable device interrupts
static inline void
intr_on()
{
w_sstatus(r_sstatus() | SSTATUS_SIE);
}
UART驅(qū)動(dòng)的top部分
如何從Shell程序輸出提示符“$ ”到Console.
在 init.c 的 main函數(shù)創(chuàng)建了一個(gè)代表Console的設(shè)備茸俭。這里通過(guò)mknod操作創(chuàng)建了console設(shè)備。因?yàn)檫@是第一個(gè)打開(kāi)的文件安皱,所以這里的文件描述符0调鬓。之后通過(guò)dup創(chuàng)建stdout和stderr。這里實(shí)際上通過(guò)復(fù)制文件描述符0酌伊,得到了另外兩個(gè)文件描述符1腾窝,2。最終文件描述符0居砖,1虹脯,2都用來(lái)代表Console。
if(open("console", O_RDWR) < 0){
mknod("console", CONSOLE, 0);
open("console", O_RDWR);
}
dup(0); // stdout
dup(0); // stderr
盡管Console背后是UART設(shè)備奏候,但是從應(yīng)用程序來(lái)看循集,它就像是一個(gè)普通的文件。Shell程序只是向文件描述符2寫(xiě)了數(shù)據(jù)蔗草。
else if(f->type == FD_DEVICE){
if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
return -1;
ret = devsw[f->major].write(1, addr, n);
}
在filewrite函數(shù)中首先會(huì)判斷文件描述符的類型咒彤。mknod生成的文件描述符屬于設(shè)備(FD_DEVICE),而對(duì)于設(shè)備類型的文件描述符咒精,我們會(huì)為這個(gè)特定的設(shè)備執(zhí)行設(shè)備相應(yīng)的write函數(shù)镶柱。因?yàn)槲覀儸F(xiàn)在的設(shè)備是Console,所以我們知道這里會(huì)調(diào)用console.c中的consolewrite函數(shù)模叙。
void
consoleinit(void)
{
initlock(&cons.lock, "cons");
uartinit();
// connect read and write system calls
// to consoleread and consolewrite.
devsw[CONSOLE].read = consoleread;
devsw[CONSOLE].write = consolewrite;
}
//
// user write()s to the console go here.
//
int
consolewrite(int user_src, uint64 src, int n)
{
int i;
for(i = 0; i < n; i++){
char c;
if(either_copyin(&c, user_src, src+i, 1) == -1)
break;
uartputc(c);
}
return i;
}
這里先通過(guò)either_copyin將字符拷入歇拆,之后調(diào)用uartputc函數(shù)。uartputc函數(shù)將字符寫(xiě)入給UART設(shè)備向楼,所以你可以認(rèn)為consolewrite是一個(gè)UART驅(qū)動(dòng)的top部分查吊。uart.c文件中的uartputc函數(shù)會(huì)實(shí)際的打印字符。
void
uartputc(int c)
{
acquire(&uart_tx_lock);
if(panicked){
for(;;)
;
}
while(uart_tx_w == uart_tx_r + UART_TX_BUF_SIZE){
// buffer is full.
// wait for uartstart() to open up space in the buffer.
sleep(&uart_tx_r, &uart_tx_lock);
}
uart_tx_buf[uart_tx_w % UART_TX_BUF_SIZE] = c;
uart_tx_w += 1;
uartstart();
release(&uart_tx_lock);
}
在UART的內(nèi)部會(huì)有一個(gè)buffer用來(lái)發(fā)送數(shù)據(jù)湖蜕,buffer的大小是32個(gè)字符逻卖。同時(shí)還有一個(gè)為consumer提供的讀指針和為producer提供的寫(xiě)指針,來(lái)構(gòu)建一個(gè)環(huán)形的buffer.昭抒。如果讀寫(xiě)指針相同评也,那么buffer是空的炼杖,如果寫(xiě)指針加1等于讀指針,那么buffer滿了.
Shell是producer盗迟,所以需要調(diào)用uartputc函數(shù).因?yàn)樘崾痉?”是我們送出的第一個(gè)字符坤邪。所以代碼會(huì)走到while外面,字符會(huì)被送到buffer中罚缕,更新寫(xiě)指針艇纺,之后再調(diào)用uartstart函數(shù)。
// if the UART is idle, and a character is waiting
// in the transmit buffer, send it.
// caller must hold uart_tx_lock.
// called from both the top- and bottom-half.
void
uartstart()
{
while(1){
if(uart_tx_w == uart_tx_r){
// transmit buffer is empty.
return;
}
if((ReadReg(LSR) & LSR_TX_IDLE) == 0){
// the UART transmit holding register is full,
// so we cannot give it another byte.
// it will interrupt when it's ready for a new byte.
return;
}
int c = uart_tx_buf[uart_tx_r % UART_TX_BUF_SIZE];
uart_tx_r += 1;
// maybe uartputc() is waiting for space in the buffer.
wakeup(&uart_tx_r);
WriteReg(THR, c);
}
}
uartstart就是通知設(shè)備執(zhí)行操作邮弹。首先是檢查當(dāng)前設(shè)備是否空閑黔衡,如果空閑的話,我們會(huì)從buffer中讀出數(shù)據(jù)腌乡,然后將數(shù)據(jù)寫(xiě)入到THR(Transmission Holding Register)發(fā)送寄存器盟劫。這里相當(dāng)于告訴設(shè)備,我這里有一個(gè)字節(jié)需要你來(lái)發(fā)送与纽。
與此同時(shí)侣签,UART設(shè)備會(huì)將數(shù)據(jù)送出。在某個(gè)時(shí)間點(diǎn)急迂,我們會(huì)收到中斷影所,因?yàn)槲覀冎霸O(shè)置了要處理UART設(shè)備中斷。接下來(lái)我們看一下袋毙,當(dāng)發(fā)生中斷時(shí)型檀,實(shí)際會(huì)發(fā)生什么冗尤。
UART驅(qū)動(dòng)的bottom部分
在trap.c的devintr函數(shù)中听盖,首先會(huì)通過(guò)SCAUSE寄存器判斷當(dāng)前中斷是否是來(lái)自于外設(shè)的中斷。如果是的話裂七,再調(diào)用plic_claim函數(shù)來(lái)獲取中斷皆看。
int
devintr()
{
uint64 scause = r_scause();
if((scause & 0x8000000000000000L) &&
(scause & 0xff) == 9){
// this is a supervisor external interrupt, via PLIC.
// irq indicates which device interrupted.
int irq = plic_claim();
if(irq == UART0_IRQ){
uartintr();
} else if(irq == VIRTIO0_IRQ){
virtio_disk_intr();
} else if(irq){
printf("unexpected interrupt irq=%d\n", irq);
}
plic_claim函數(shù)位于plic.c文件中。在這個(gè)函數(shù)中背零,當(dāng)前CPU核會(huì)告知PLIC腰吟,自己要處理中斷,PLIC_CLAIM會(huì)將中斷號(hào)返回徙瓶,對(duì)于UART來(lái)說(shuō)毛雇,返回的中斷號(hào)是10。
// handle a uart interrupt, raised because input has
// arrived, or the uart is ready for more output, or
// both. called from devintr().
void
uartintr(void)
{
// read and process incoming characters.
while(1){
int c = uartgetc();
if(c == -1)
break;
consoleintr(c);
}
// send buffered characters.
acquire(&uart_tx_lock);
uartstart();
release(&uart_tx_lock);
}
int
uartgetc(void)
{
if(ReadReg(LSR) & 0x01){
// input data is ready.
return ReadReg(RHR);
} else {
return -1;
}
}
為我們現(xiàn)在還沒(méi)有通過(guò)鍵盤(pán)輸入任何數(shù)據(jù)灵疮,所以UART的接受寄存器現(xiàn)在為空震捣。代碼會(huì)直接運(yùn)行到uartstart函數(shù),這個(gè)函數(shù)會(huì)將Shell存儲(chǔ)在buffer中的任意字符送出润樱。實(shí)際上在提示符“$”之后壹若,Shell還會(huì)輸出一個(gè)空格字符,write系統(tǒng)調(diào)用可以在UART發(fā)送提示符“$”的同時(shí)皂冰,并發(fā)的將空格字符寫(xiě)入到buffer中舌稀。所以UART的發(fā)送中斷觸發(fā)時(shí),可以發(fā)現(xiàn)在buffer中還有一個(gè)空格字符灼擂,之后會(huì)將這個(gè)空格字符送出壁查。
UART連接了兩個(gè)設(shè)備,一個(gè)是鍵盤(pán)剔应,另一個(gè)是顯示設(shè)備睡腿,也就是Console。
UART讀取鍵盤(pán)輸入
有時(shí)Shell會(huì)調(diào)用read從鍵盤(pán)中讀取字符峻贮。 在read系統(tǒng)調(diào)用的底層席怪,會(huì)調(diào)用fileread函數(shù)。在這個(gè)函數(shù)中纤控,如果讀取的文件類型是設(shè)備挂捻,會(huì)調(diào)用相應(yīng)設(shè)備的read函數(shù)。
int
consoleread(int user_dst, uint64 dst, int n)
{
uint target;
int c;
char cbuf;
target = n;
acquire(&cons.lock);
while(n > 0){
// wait until interrupt handler has put some
// input into cons.buffer.
while(cons.r == cons.w){
if(killed(myproc())){
release(&cons.lock);
return -1;
}
sleep(&cons.r, &cons.lock);
}
c = cons.buf[cons.r++ % INPUT_BUF_SIZE];
if(c == C('D')){ // end-of-file
if(n < target){
// Save ^D for next time, to make sure
// caller gets a 0-byte result.
cons.r--;
}
break;
}
// copy the input byte to the user-space buffer.
cbuf = c;
if(either_copyout(user_dst, dst, &cbuf, 1) == -1)
break;
dst++;
--n;
if(c == '\n'){
// a whole line has arrived, return to
// the user-level read().
break;
}
}
release(&cons.lock);
return target - n;
}
所以在某個(gè)時(shí)間點(diǎn)船万,假設(shè)用戶通過(guò)鍵盤(pán)輸入了“l(fā)”声怔,這會(huì)導(dǎo)致“l(fā)”被發(fā)送到主板上的UART芯片,產(chǎn)生中斷之后再被PLIC路由到某個(gè)CPU核,之后會(huì)觸發(fā)devintr函數(shù),devintr可以發(fā)現(xiàn)這是一個(gè)UART中斷慨亲,然后通過(guò)uartgetc函數(shù)獲取到相應(yīng)的字符,之后再將字符傳遞給consoleintr函數(shù)。
// read one input character from the UART.
// return -1 if none is waiting.
int
uartgetc(void)
{
if(ReadReg(LSR) & 0x01){
// input data is ready.
return ReadReg(RHR);
} else {
return -1;
}
}
// handle a uart interrupt, raised because input has
// arrived, or the uart is ready for more output, or
// both. called from devintr().
void
uartintr(void)
{
// read and process incoming characters.
while(1){
int c = uartgetc();
if(c == -1)
break;
consoleintr(c);
}
// send buffered characters.
acquire(&uart_tx_lock);
uartstart();
release(&uart_tx_lock);
}
//
// the console input interrupt handler.
// uartintr() calls this for input character.
// do erase/kill processing, append to cons.buf,
// wake up consoleread() if a whole line has arrived.
//
void
consoleintr(int c)
{
acquire(&cons.lock);
switch(c){
case C('P'): // Print process list.
procdump();
break;
case C('U'): // Kill line.
while(cons.e != cons.w &&
cons.buf[(cons.e-1) % INPUT_BUF_SIZE] != '\n'){
cons.e--;
consputc(BACKSPACE);
}
break;
case C('H'): // Backspace
case '\x7f': // Delete key
if(cons.e != cons.w){
cons.e--;
consputc(BACKSPACE);
}
break;
default:
if(c != 0 && cons.e-cons.r < INPUT_BUF_SIZE){
c = (c == '\r') ? '\n' : c;
// echo back to the user.
consputc(c);
// store for consumption by consoleread().
cons.buf[cons.e++ % INPUT_BUF_SIZE] = c;
if(c == '\n' || c == C('D') || cons.e-cons.r == INPUT_BUF_SIZE){
// wake up consoleread() if a whole line (or end-of-file)
// has arrived.
cons.w = cons.e;
wakeup(&cons.r);
}
}
break;
}
release(&cons.lock);
}
字符會(huì)通過(guò)consputc片橡,輸出到console上給用戶查看。之后,字符被存放在buffer中舆吮。在遇到換行符的時(shí)候色冀,喚醒之前sleep的進(jìn)程敌卓,也就是Shell趟径,再?gòu)腷uffer中將數(shù)據(jù)讀出蜗巧。
所以這里也是通過(guò)buffer將consumer和producer之間解耦幕屹,這樣它們才能按照自己的速度,獨(dú)立的并行運(yùn)行挫鸽。如果某一個(gè)運(yùn)行的過(guò)快了,那么buffer要么是滿的要么是空的枫匾,consumer和producer其中一個(gè)會(huì)sleep并等待另一個(gè)追上來(lái)干茉。
Lab: Copy-on-Write Fork for xv6
這個(gè)lab還是非常耗時(shí)的等脂,因?yàn)殡y在他是第一次有了race condition。 如果一開(kāi)始沒(méi)有想清楚争涌,多個(gè)進(jìn)程并發(fā)的情況模软,就很容易陷入痛苦的debug中。但想清楚后继蜡,核心代碼非常簡(jiǎn)潔仅颇。
代碼分為2部分忘瓦。
第一部分境蜕,是對(duì)每個(gè)物理頁(yè)分配一個(gè)引用計(jì)數(shù)器汽摹。然后更新kalloc.c 每個(gè)函數(shù)逼泣,去正確配置他們,因?yàn)闀?huì)涉及多進(jìn)程并行操作氏仗,所以需要上鎖皆尔。
第二部分,是我們需要在uvmcopy中(fork調(diào)用)流炕,針對(duì)有寫(xiě)權(quán)限的頁(yè)每辟,進(jìn)行抹除父子頁(yè)上的寫(xiě)權(quán)限標(biāo)志。然后添加COW標(biāo)志挠将。同時(shí)增加該頁(yè)指向的物理地址的引用計(jì)數(shù)捐名。然后在發(fā)生page fault中斷成艘,或copyout時(shí),進(jìn)行寫(xiě)時(shí)復(fù)制拂酣。
optional challenge
Measure how much your COW implementation reduces the number of bytes xv6 copies and the number of physical pages it allocates. Find and exploit opportunities to further reduce those numbers.
我們可以看到上面的做法赵颅,保證了線程安全性,但是我們寫(xiě)一個(gè)函數(shù)去統(tǒng)計(jì)節(jié)約的頁(yè)面和字節(jié)復(fù)制的開(kāi)銷募寨,其實(shí)是不夠理想的拔鹰。我們先在kalloc里定義2個(gè)新函數(shù)。
創(chuàng)建一個(gè)新的系統(tǒng)調(diào)用
然后我們?cè)赨VMCOPY,可以去節(jié)約頁(yè)面復(fù)制開(kāi)銷决采,但是在copyonwrite的時(shí)候树瞭,是要把節(jié)約的開(kāi)銷給還回去晒喷。
我們可以看到在simpletest的時(shí)候,是節(jié)約了不少開(kāi)銷势决。但是在做three test的時(shí)候,我們不但沒(méi)有節(jié)約,反而還比之前付出了更多的頁(yè)面開(kāi)銷独柑。
這里進(jìn)一步的優(yōu)化空間就在于迈窟,當(dāng)引用計(jì)數(shù)降為1時(shí),我們依然保持了kalloc 加 kfree的策略群嗤。也就是說(shuō)我們有2個(gè)進(jìn)程菠隆,做fork的時(shí)候,假設(shè)節(jié)約了4頁(yè)狂秘。但是這4頁(yè)之后都被父進(jìn)程和子進(jìn)程寫(xiě)的話骇径。父子進(jìn)程都會(huì)進(jìn)到copyonwrite里,kalloc+kfree者春,這樣復(fù)制了8頁(yè)晰筛,反而消耗更多了怜瞒。
簡(jiǎn)單的加一個(gè)讀取引用計(jì)數(shù),判斷是否為1脾歇,如果為1踢京,就不alloc, 直接設(shè)置WRITE權(quán)限的做法,會(huì)造成測(cè)試不通過(guò)。這里我調(diào)了很久够话,又是race condition的問(wèn)題。
問(wèn)題如下:
父進(jìn)程讀取當(dāng)前引用計(jì)數(shù)為2谷徙;決定采取kalloc+kfree的方法册着。然后schedule到子進(jìn)程
子進(jìn)程讀取當(dāng)前引用計(jì)數(shù)為2;決定采取kalloc+kfree的方法钦奋。然后schedule到父進(jìn)程
然后調(diào)用完kfree 2次,其中第二次理疙,還是把原先的那頁(yè)給free了豌熄。所以依然會(huì)有這個(gè)問(wèn)題巷折。
為了解決上述問(wèn)題:
我們需要保證當(dāng)一個(gè)進(jìn)程在讀取引用計(jì)數(shù)并且決定kalloc時(shí)上鞠,另一個(gè)進(jìn)程在讀取引用計(jì)數(shù)時(shí)拿不到鎖萧锉。當(dāng)對(duì)面搞定了,才會(huì)把鎖釋放谦炒。然后這樣就不會(huì)發(fā)生上述情況了还蹲。 當(dāng)然里面還有非常多的細(xì)節(jié)要考慮诵次。比如kalloc 內(nèi)存分配不夠。我的改動(dòng)大概如下:
-
新加了一個(gè)kalloc_cow,專門(mén)為copyonwrite函數(shù)調(diào)度。里面會(huì)處理江解,如果引用計(jì)數(shù)是1桨螺,就是不復(fù)制,直接返回之前的地址酿秸,同時(shí)賦值WRITE權(quán)限灭翔。
-
修改copyonwrite函數(shù),根據(jù)返回的地址新舊走不同的邏輯允扇。
我們來(lái)看下新方法的效果:
可以看到這次的節(jié)約是遞增的缠局,就是我們最壞情況也是很原來(lái)的分配頁(yè)面開(kāi)銷一樣。不會(huì)出現(xiàn)反而要花更多開(kāi)銷的情況考润。
進(jìn)一步優(yōu)化,實(shí)現(xiàn)lazy allocation
我們?cè)賮?lái)看一下新的效果:
-
首先確保功能性測(cè)試都能通過(guò):
simpletest 沒(méi)有大的提升主要是因?yàn)閟brk之后读处,幾乎把每個(gè)頁(yè)都寫(xiě)了糊治。把寫(xiě)的邏輯注釋掉,我們可以再看下效果: