实验纲要
抢占多任务处理
这个其实设置好时钟中断
(属于设备中断or外部中断)。为每个在CPU上执行的进程分配好时间片。如果时间片用完进程还没主动放弃CPU,那么时钟中断处理程序
就要调用轮询调度程序sched_yield()
将CPU给其他进程。要注意lapic
的管理,以及外部中断入口是IDT 32-47
进程间通信IPC(以sendpage执行流程为例)
首先说下,IPC
是Inter-Process Communication。PIC
是Programmable Interrupt Control。
这里非常秀的是两个系统调用sys_ipc_try_send()和sys_ipc_recv()
之间的配合,真的是国民老公与傲娇老婆的完美搭配。或者就是一对好基友。
直接从进入sendpage.c/umain()开始说起。怎么进入的请看Lab3:User Environments
//sendpage.c/umain()
//只启动了一个CPU
父进程:who=fork(),产生子进程,两个进程基本一样,
-->下一条语句都是if(who==0){...}
-->父进程:
-->sys_page_alloc(thisenv->env_id, TEMP_ADDR, PTE_P | PTE_W | PTE_U);
-->memcpy(TEMP_ADDR, str1, strlen(str1) + 1);
-->ipc_send(who, 0, TEMP_ADDR, PTE_P | PTE_W | PTE_U);此时who是子进程id
-->r=sys_ipc_try_send(to_env, val, pg, perm);
-->由于子进程不是接收状态,得到r=-E_IPC_NOT_RECV
-->sys_yield()主动放弃CPU,所以子进程得到CPU
-->子进程进入if循环:
-->ipc_recv(&who, TEMP_ADDR_CHILD, 0); 此时who是from_env,是父进程id
-->r=sys_ipc_recv(pg);进入接收状态并让出CPU
设好'dstva','from=0'证明还没环境发送成功,'recving=1,stats=ENV_NOT_RUNABLE'锁住直到接到"消息"
-->父进程此时还在轮询sys_ipc_try_send():
-->r=sys_ipc_try_send(to_env, val, pg, perm);
发现子进程进入接收状态,在对自己进行`详细审查`后才准备发"消息",
发送"消息"成功了,它会贴心的帮recvenv设置好'env_ipc_*',
并让'recvenv->env_status=ENV_RUNNABLE',甚至给`recvenv的%eax赋值0`提醒recvenv它收到"消息"了
-->发送成功,返回0,退出ipc_send()
-->ipc_recv(&who, TEMP_ADDR, 0);
-->r=sys_ipc_recv(pg);进入接收状态并让出CPU
-->子进程还在ipc_recv()里。不过此时已经接收"页面"并映射到TEMP_ADDR_CHILD了,并由父进程的sys_ipc_try_send修复好了状态
-->*from_env_store=父进程id
-->*perm_store=perm
-->cprintf("%x got message: %s\n", who, TEMP_ADDR_CHILD);打印处接收到的信息
-->cprintf("child received correct message\n");验证信息是否正确
-->memcpy(TEMP_ADDR_CHILD, str2, strlen(str2) + 1);向TEMP_ADDR_CHILD写入新内容str2
-->ipc_send(who, 0, TEMP_ADDR_CHILD, PTE_P | PTE_W | PTE_U);
将TEMP_ADDR_CHILD对应页面发给父进程。此时的who由子进程的ipc_recv()赋值了父进程id
并且此时父进程已经由ipc_recv()进入了接收状态,所以子进程可以直接发送成功,不用让出CPU
-->return
-->exit()那子进程到这就exit gracefully了,寿终正寝
-->父进程还在ipc_recv()里:也已经收到子进程的信息了,所以退出ipc_recv(),且此时who又指向了子进程id
-->cprintf("%x got message: %s\n", who, TEMP_ADDR_CHILD);打印处接收到的信息
-->cprintf("child received correct message\n");验证信息是否正确
-->return
-->exit()至此,父进程也完成任务exit gracefully
-->monitor()CPU由于没有进程可执行,在sched_halted里进入monitor
实验过程
在lab 4的最后一部分中,您将修改内核以抢占
不合作的环境,并允许环境之间显式
地传递消息。
Clock Interrupts and Preemption
运行user/spin测试程序。这个测试程序派生出(forks off)一个子环境,一旦它接收到对CPU的控制,它就会永远在一个紧密的循环中旋转。无论是父环境还是内核都不会重新获得CPU。就保护系统不受用户模式环境中的bug或恶意代码的影响而言,这显然不是理想的情况,因为任何用户模式环境都可以通过进入无限循环而永远不释放CPU
来停止整个系统。为了让内核抢占(preempt
)一个正在运行的环境,强制重新控制CPU,我们必须扩展JOS内核,以支持来自时钟硬件的外部硬件中断
。
Interrupt discipline(纪律,规则?)
外部中断
(i.e., device interrupts)称为IRQs
(Interrupt Requests)。有16种可能的IRQs,编号从0到15。从IRQ编号到IDT条目的映射不是固定的(fixed)。picirq.c中的pic_init
通过IRQ_OFFSET+15将IRQs 0 -15映射到IDT条目IRQ_OFFSET+15。
在inc/trap.h,IRQ_OFFSET被定义为十进制数32。因此,IDT条目32-47对应于IRQs 0-15。例如,时钟中断是IRQ 0。因此IDT[IRQ_OFFSET + 0] (i.e., IDT[32])
在内核中包含时钟中断处理程序的地址。使用IRQ_OFFSET,以便设备中断不会与处理器异常重叠
,这显然会导致混淆。(事实上,在运行MS-DOS的PCs的早期,IRQ_OFFSET实际上是零,这确实在处理硬件中断和处理处理器异常之间造成了巨大的混淆!)
在JOS中,与xv6 Unix相比,我们进行了关键的简化。外部设备中断在内核中总是禁用的(和xv6一样,在用户空间中启用)。外部中断由%eflags寄存器的FL_IF标志位
控制(参见inc/mmu.h)。设置了这个位之后,就启用了外部中断。虽然可以通过几种方式修改它,但是由于我们的简化,我们将仅通过在进入和离开用户模式时
保存和恢复%eflags寄存器的过程来处理它。
您将必须确保在用户环境中设置FL_IF标志
,以便当中断到达时,它将被传递到处理器
并由中断代码处理。否则,中断将被屏蔽,或者忽略,直到重新启用中断。我们用引导加载程序的第一条指令来屏蔽中断,到目前为止我们还没有重新启用它们(惊呆了!!!)。
CLI
(Clear Interrupt-Enable Flag) and STI
(Set Interrupt-Enable Flag)
Exercise 13. 修改
kern/trapentry.S和kern/trap.c
去初始化IDT中的适当条目,并为IRQs 0~15提供处理程序。然后修改kern/env.c/env_alloc()
里的代码,确保始终在启用中断的情况下运行用户环境。还需要在
sched_halt()
中取消对sti指令的注释(uncomment),以便空闲(idle)的cpu取消中断掩码。处理器在调用硬件中断处理程序时从不推送错误代码。此时,您可能希望重新阅读 80386 Reference Manual的9.2节,或 IA-32 Intel Architecture Software Developer’s Manual, Volume 3的5.8节。
在完成这个练习之后,如果您使用任何运行的时间不短(例如,spin)的测试程序运行内核,您应该会看到
内核为硬件中断打印trap frames
。中断现在已在处理器中启用,但JOS还没有处理它们,所以您应该看到它将每个中断错误地归因于当前正在运行的用户环境并销毁它。最终,它应该销毁所有环境并进入monitor。
初始化IDT相关条目,注册处理程序
//inc/trap.h
void irq_error_handler();
void irq_kbd_handler();
void irq_ide_handler();
void irq_timer_handler();
void irq_spurious_handler();
void irq_serial_handler();
//kern/trap.c/trap_init()
//在inc/trap.h中给出了哪几种Hardware IRQ numbers.
//外部设备中断在内核中总是禁用的(和xv6一样,在用户空间中启用)
SETGATE(idt[IRQ_OFFSET+IRQ_ERROR], 0, GD_KT, irq_error_handler, 3);
SETGATE(idt[IRQ_OFFSET+IRQ_IDE], 0, GD_KT, irq_ide_handler, 3);
SETGATE(idt[IRQ_OFFSET+IRQ_KBD], 0, GD_KT, irq_kbd_handler, 3);
SETGATE(idt[IRQ_OFFSET+IRQ_SERIAL], 0, GD_KT, irq_serial_handler, 3);
SETGATE(idt[IRQ_OFFSET+IRQ_SPURIOUS], 0, GD_KT, irq_spurious_handler, 3); //spurious假的,伪造的
SETGATE(idt[IRQ_OFFSET+IRQ_TIMER], 0, GD_KT, irq_timer_handler, 3);
//trapentry.S
TRAPHANDLER_NOEC(irq_error_handler, IRQ_OFFSET+IRQ_ERROR);
TRAPHANDLER_NOEC(irq_ide_handler, IRQ_OFFSET+IRQ_IDE);
TRAPHANDLER_NOEC(irq_kbd_handler, IRQ_OFFSET+IRQ_KBD);
TRAPHANDLER_NOEC(irq_serial_handler, IRQ_OFFSET+IRQ_SERIAL);
TRAPHANDLER_NOEC(irq_spurious_handler, IRQ_OFFSET+IRQ_SPURIOUS);
TRAPHANDLER_NOEC(irq_timer_handler, IRQ_OFFSET+IRQ_TIMER);
外部设备中断在内核中总是禁用的(和xv6一样,在用户空间中启用),所以SETDATE时才需要把DPL设成3?
修改kern/env.c/env_alloc()里的代码,确保始终在启用中断的情况下运行用户环境。外部中断由%eflags寄存器的FL_IF标志位
控制。
// Enable interrupts while in user mode.
// LAB 4: Your code here.
e->env_tf.tf_eflags |= FL_IF;
Handling Clock Interrupts
在user/spin程序中,子环境第一次运行之后,它只是在循环中旋转,内核再也没有得到控制权。我们需要对硬件进行编程,周期性地生成时钟中断
,这将迫使内核拿回控制权,在内核中我们可以将控制切换到其他的用户环境。
我们为您编写的对lapic_init和pic_init
(来自init.c中的i386_init)的调用设置了时钟和中断控制器来生成中断
。现在需要编写代码来处理这些中断。
Exercise 14. 修改内核的trap_dispatch()函数,使其
调用sched_yield()
,以便在发生时钟中断时查找并运行不同的环境。
您现在应该能够让user/spin测试工作:父环境应该调用sys_yield()将控制权传递给子环境几次,但每次都在一个时间片后重新获得对CPU的控制,最后杀死子环境并terminate gracefully。
// Handle spurious interrupts
// The hardware sometimes raises these because of noise on the
// IRQ line or other reasons. We don't care.
else if(tf->tf_trapno == IRQ_OFFSET+IRQ_TIMER){
lapic_eoi(); //??承认中断?或者说告诉lapic收到中断了?这个是真想不到
sched_yield();
}
void lapic_eoi(void)
{
if (lapic)
lapicw(EOI, 0);// Ack any outstanding(未解决的,未完成的) interrupts.
}
但是我不知道为什么一直panic在trap()中的assert(!(read_eflags() & FL_IF));
什么时候在内核态启用中断了?搞得我只能注释掉这句。但是很明显注释掉是不行的,原来是在SETGATE的时候istrap参数我很多设成了1,如SETGATE(idt[T_PGFLT], 1, GD_KT, pgflt_handler, 0);
陷阱门是不会重置FL_IF位的,导致内核态下中断没关,自然过不了这条assert。
这是做一些回归测试(regression testing)的好时机。确保通过启用中断,您没有破坏lab中以前工作的任何部分(例如forktree)。另外,尝试使用make CPUS =2 target
运行多个cpu。你现在也应该能够通过stresssched测试
了。你现在应该在这个lab中得到65/80分。
Inter-Process communication (IPC)
(从技术上讲,在JOS中这是“inter-environment communication”或“IEC”,但其他人都称之为IPC,所以我们将使用标准术语)
我们一直关注操作系统的隔离(isolation
)方面,它让每个程序都以为机器完全属于自己
。操作系统的另一个重要功能是允许程序在需要的时候彼此通信
。让程序与其他程序交互
是非常强大的。Unix管道模型就是一个典型的例子。
进程间通信有许多模型。即使在今天,关于哪种模型是最好的仍然存在争议。我们不讨论这个问题。相反,我们将实现一个简单的IPC机制,然后进行测试。
IPC in JOS
您将实现几个附加的JOS内核系统调用,它们共同提供了一个简单的进程间通信机制。您将实现两个系统调用sys_ipc_recv和sys_ipc_try_send
。然后您将实现两个库封装(library wrappers)ipc_recv和ipc_send
。
用户环境可以使用JOS的IPC机制相互发送的“消息”
由两个部分组成:一个32位的值,以及一个页面映射
。允许环境在消息中传递页面映射
提供了一种高效的方法来传输比单个32位整数更大的数据
,还允许环境轻松地设置共享内存
安排。
Sending and Receiving Messages
要接收消息,环境调用sys_ipc_recv。此系统调用取消当前环境的调度(de-schedules
),并且在收到消息之前不会再次运行
它。当一个环境在等待接收消息时,任何其他环境
都可以向它发送消息——不仅仅是一个特定的环境,也不仅仅是与接收环境有父/子关系的环境。换句话说,您在第A部分中实现的权限检查
将不适用于IPC,因为IPC系统调用是经过精心设计的,是“安全”
的:一个环境不能仅仅通过发送消息就导致另一个环境发生故障(除非目标环境也有bug)。
要尝试发送一个值,环境使用接收者的环境id和要发送的值
调用sys_ipc_try_send
。如果指定的环境实际正在接收状态(它调用了sys_ipc_recv
,但还没有获得值
),那么send将传递消息并返回0。否则,send返回-E_IPC_NOT_RECV,表示目标环境当前不期望接收值。
用户空间中的库函数ipc_recv
将负责调用sys_ipc_recv
,然后在当前环境的struct Env
中查找关于接收值的信息。
类似地,库函数ipc_send
将负责重复调用sys_ipc_try_send
,直到发送成功。
Transferring Pages
当环境使用有效的dstva参数(UTOP以下)
调用sys_ipc_recv时,环境声明它愿意接收页面映射
。如果发送方发送了一个页面,那么该页面应该映射到接收方地址空间中的dstva。如果接收方已经在dstva上映射了一个页面,则将前一个页面映射取消
。
当环境使用有效的srcva (UTOP以下)
调用sys_ipc_try_send时,这意味着发送方希望将当前映射在srcva的权限为perm的页面
发送给接收方。成功完成IPC之后,发送方将页面的原始映射保存在地址空间中的srcva,但是接收方也在接收方的地址空间中获得相同物理页面的映射,该映射位于接收方最初指定的dstva。因此,此页面
在发送方和接收方之间共享
。
如果发送方或接收方都没有表示应该传输页面,则不传输任何页面。在任何IPC之后,内核将接收方的Env结构中的新字段env_ipc_perm
设置为接收页的权限
,如果没有接收页,则设置为零。
Implementing IPC
Exercise 15. 在kern/syscall.c中实现
sys_ipc_recv和sys_ipc_try_send
。在实现它们之前,请阅读关于它们的注释
,因为它们必须一起工作。在这些例程中调用envid2env时,应该将checkperm标志设置为0
,这意味着任何环境都允许向任何其他环境发送IPC消息,内核除了验证目标envid是有效的
外,不进行任何特殊权限检查。
然后在lib/ipc.c中实现ipc_recv和ipc_send
库函数。
使用user/pingpong和user/primes
函数来测试IPC机制。 user/primes(质数) 将为每个质数生成一个新环境,直到耗尽JOS环境为止。您可能会发现,阅读user/prime .c以了解所有的分叉(forking)和IPC在幕后进行是很有趣的。
sys_ipc_recv()
curenv进入接收状态(设好dstva
,from=0
证明还没环境发送成功,recving=1,stats=ENV_NOT_RUNABLE
锁住直到接到"消息"),并让出CPU。
要注意的是,除非发生error,否则sys_ipc_recv()是没有返回值
的。也就是说curenv的%eax将会没有返回值,那怎么办呢?这老婆够傲娇!
不用担心,sys_ipc_try_send为你解决一切烦恼。在对自己进行详细审查
后才准备发"消息",如果sendenv发送“消息”成功
了,它会贴心的帮recvenv设置好env_ipc_*
,并让recvenv->env_status=ENV_RUNNABLE
,甚至给recvenv的%eax赋值0
提醒recvenv它收到"消息"了,真是国民好老公啊!
static int sys_ipc_recv(void *dstva){
// LAB 4: Your code here.
if((uintptr_t)dstva<UTOP){
if((uintptr_t)dstva%PGSIZE)
return -E_INVAL;
}
// Block until a value is ready??
curenv->env_ipc_dstva=dstva;
curenv->env_ipc_from = 0; //证明现在还没有收到任何信息
curenv->env_ipc_recving=1; //1-block, 0-unblock
// mark yourself not runnable, and then give up the CPU.
curenv->env_status = ENV_NOT_RUNNABLE;
// This function only returns on error, but the system call will eventually
// return 0 on success.
// 也就是说如果成功了,syscall()是不会有返回值的,
//而又需要%eax返回0证明成功了,就需要sys_ipc_try_send来设置其%eax
/*if(curenv->env_ipc_value){
//不需要自己unblock,sys_ipc_try_send成功会把这设0的
//不然会在pingpong测试里卡住,卡在get 2 就不动了
curenv->env_ipc_recving=0;
}*/
sched_yield();
/*panic("sys_ipc_recv not implemented");
return 0;*/
}
sys_ipc_try_send()
这个系统调用的作用在它的傲娇老婆sys_ipc_recv那里介绍完了。
不过我有点疑问,映射srcva对应页面到recvenv->env_ipc_dstva处,为什么用sys_page_map
会报错bad environment
。明明两个envid都是合理的,却过不了envid2env。
static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
// LAB 4: Your code here.
int r ;
struct Env *e;
pte_t *pte;
struct PageInfo *srcpp;
//cprintf("curenv:%08x send to:%08x\n",curenv->env_id,envid);
r=envid2env(envid, &e, 0);
if(r<0)
return r;
//e->env_ipc_perm=0;这样写是错的
if((uintptr_t)srcva >= UTOP)
perm = 0;
if(!e->env_ipc_recving || e->env_ipc_from)
return -E_IPC_NOT_RECV;
if(perm){//之前以if((uintptr_t)srcva < UTOP)为条件,那perm本身就存在为0的情况,
//会导致page_insert()时映射到dstva处的页面权限错误
if((uintptr_t)srcva%PGSIZE)
return -E_INVAL;
//PTE_U | PTE_P must be set
if((perm & (PTE_U | PTE_P)) ==0)
return -E_INVAL;
//PTE_AVAIL | PTE_W may or may not be set, but no other bits may be set
if( perm & ~PTE_SYSCALL )
return -E_INVAL;
//-E_INVAL if srcva < UTOP but srcva is not mapped in the caller's address space.
srcpp=page_lookup(curenv->env_pgdir, srcva, &pte);
if(srcpp==NULL)
return -E_INVAL;
if(!pte || ((perm & PTE_W)&&(*pte & 0x800)))
return -E_INVAL;
//if there's not enough memory to map srcva in envid's address space.
//r=sys_page_map(curenv->env_id, srcva, envid, e->env_ipc_dstva, perm);为什么这个不行
//虽然我知道sys_page_map里最后就是page_insert,前面的检查也与这里上面大多重复了
//但是还是不明白为什么会报错bad environment,envid都是合理的呀
r=page_insert(e->env_pgdir, srcpp, e->env_ipc_dstva, perm);
if(r<0){
return r;
}
e->env_ipc_perm=perm;
}
e->env_ipc_recving=0;
e->env_ipc_from=curenv->env_id;
e->env_ipc_value=value;
// The target environment is marked runnable again, returning 0
e->env_status = ENV_RUNNABLE;
//不加这个就报错[00001001] user panic in <unknown> at lib/syscall.c:35: syscall 12 returned 12 (> 0)
//因为sys_ipc_recv如果成功是没有返回值的,当运行到这里,可以证明receiver已经接收到了,帮他返回0
e->env_tf.tf_regs.reg_eax=0;
return 0;
panic("sys_ipc_try_send not implemented");
}
ipc_recv()
这个的话,除了能接收"消息"
,还有一点就是能从recvenv->env_ipc_*中把sendenv_id以及pg_perm
提取出来,也是非常有用的。
int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
// LAB 4: Your code here.
int r;
if(pg!=NULL)
r=sys_ipc_recv(pg);
else
r=sys_ipc_recv((void *)UTOP);
if(from_env_store!=NULL)
*from_env_store=thisenv->env_ipc_from;
if(perm_store!=NULL)
*perm_store=thisenv->env_ipc_perm;
if(from_env_store!=NULL&&perm_store!=NULL&&r<0){
*from_env_store=0;
*perm_store=0;
return r;
}
if(r<0)
return r;
return thisenv->env_ipc_value;
}
ipc_send()
没什么好讲的,指定接收消息的环境to_env
,向其发送一个32bit的value
,如果pg有效,还发送权限为perm的pg
,以便共享页面
。
不过注意,如果没有发送成功,它会不停发送,如果人家不肯收,它就把CPU先让出去,下次获得CPU还是会不停发送,直到成功
,真是死脑筋,死皮赖脸送人家礼物。
void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
// LAB 4: Your code here.
int r;
while(1){
if(pg)
r=sys_ipc_try_send(to_env, val, pg, perm);
else
r=sys_ipc_try_send(to_env, val, (void *)UTOP, perm);
if(r!=0){
if(r!=-E_IPC_NOT_RECV)
panic("ipc send fault:%e",r);
else
sys_yield();
}else
return;
}
panic("ipc_send not implemented");
}
两个系统调用,两个C封装函数,都不难,就是我的代码写的太臃肿了,同学写的好简洁,真是汗颜。
自问自答
1.make run-func
这条命令的执行流程是怎么样的?什么时候第一次进入trap()?因为我之前assert(!(read_eflags() & FL_IF)),我想知道什么时候assert的,没跟到。
答:我突然想起#if defined(TEST) ENV_CREATE(TEST, ENV_TYPE_USER); #else ENV_CREATE(user_primes, ENV_TYPE_USER);
那我认为make run-func指令其实是给了一个defined(FUNC)
,所以就执行ENV_CREATE(FUNC, ENV_TYPE_USER); 其他都是一样的。至于什么时候第一次进trap(),我本以为是libmain里的sys_getenvid()系统调用陷入,结果在上面输出语句失败,那我就不懂了。
2.很多明明是异常而不是中断,为什么JOS里在SETGATE
的时候非要全部设成中断呢?答:难道是为了简化?难道是因为JOS里都是通过int指令
陷入就都设成中断?难道是规定了JOS里内核态下不能开中断,所以统一把所有门都设成中断门
,这样就能在内核态下重置FL_IF位(屏蔽中断)
了
3.在系统调用sys_ipc_try_send()中,映射srcva对应页面到recvenv->env_ipc_dstva处,为什么用page_insert?为什么用sys_page_map
会报错bad environment
?
答:很明显报错bad environment是因为过不了envid2env(),那为什么在sys_ipc_try_send()也envid2env能过,但是在sys_page_map里却不能过呢?原因就是envid2env()的参数checkperm
。由于JOS的简化,收发消息是安全的
,并不会攻击到另一个环境,所以内核除了验证目标envid是有效的外,不进行任何特殊权限检查
,所以checkperm=0。而sys_page_map里就必须确保有映射权限,所以checkperm=1,自然就会报错bad environment。
而sys_page_map其实就是一些权限检查加page_insert
,sys_ipc_try_send()里已经进行过那些权限检查了,所以直接page_insert。
4.lapic的那些端口以及picirq.c的那些操作其实不是很懂。