Part C:抢占式多任务处理和进程间通信(IPC)
注:根据MIT-JOS的lab指导手册,以下不明确区分“环境”和“进程”
重要提醒:每次实现完系统调用,记得补充kern/syscall.c
的syscall()
!!!
在lab4的最后一部分,我们将修改内核以支持抢占不合作环境拥有的资源并允许进程间通信
时钟中断和抢占
如果尝试运行user/spin
,这个程序会fork
创建一个子进程,子进程一旦得到CPU的控制权就会陷入死循环,kernel和父进程将无法再次获得CPU。这显然是操作系统需要避免发生的情况,因为任何用户进程都能轻易进入死循环而永远不会再交出CPU的控制权。为了允许内核从一个正在运行的进程抢夺 CPU 资源,我们需要支持来自硬件时钟的外部硬件中断。
中断规则
外部中断(即设备中断)用IQR(Interrupt Request)表示。总共有16种可能的IQR,编号为0-15。picirq.c
中的pic_init
映射了编号0-15的外部中断到IDT表中,其表项索引为IRQ_OFFSET~IRQ_OFFSET+15
IRQ_OFFSET
被定义为32,因此IQR的中断向量为32-47,例如时钟中断的IQR编号为0,则它的中段描述符为IDT[IRQ_OFFSET+0]
,中断向量为32,IDT[32]
中包含了时钟中断的处理函数入口。
在JOS中我们相比于xv6 Unix进行了简化:在内核态时禁用外部中断(仅在在用户态时启用外部中断)。外部中断通过eflags
寄存器的FL_IF
位控制,该位为1时启用外部中断,为0时禁用。在我们的简化后,该位仅在保存和恢复eflags
寄存器,即仅在进入和离开内核时被修改。
我们必须确保在用户态时FL_IF
位被设置为1,使用户态能正常处理外部中断,否则中断将被屏蔽或忽略。现在的代码里,在bootloader一开始我们就通过cli
关闭了中断,然后再也没开启过,因此接下来要完成
- 修改
kern/trapentry.S
和kern/trap.c
,将外部中断IQR 0-15加入中断向量表,为它们编写中断处理入口 - 修改
kern/env.c
里的env_alloc()
,为用户态启用FL_IF
标识
注意:对于硬件发生的中断,处理器不会自动压入错误码
实现如下:
trapentery.S
添加
TRAPHANDLER_NOEC(iqr0, 32)
TRAPHANDLER_NOEC(iqr1, 33)
TRAPHANDLER_NOEC(iqr2, 34)
TRAPHANDLER_NOEC(iqr3, 35)
TRAPHANDLER_NOEC(iqr4, 36)
TRAPHANDLER_NOEC(iqr5, 37)
TRAPHANDLER_NOEC(iqr6, 38)
TRAPHANDLER_NOEC(iqr7, 39)
TRAPHANDLER_NOEC(iqr8, 40)
TRAPHANDLER_NOEC(iqr9, 41)
TRAPHANDLER_NOEC(iqr10, 42)
TRAPHANDLER_NOEC(iqr11, 43)
TRAPHANDLER_NOEC(iqr12, 44)
TRAPHANDLER_NOEC(iqr13, 45)
TRAPHANDLER_NOEC(iqr14, 46)
TRAPHANDLER_NOEC(iqr15, 47)
trap.c
的trap_init()
添加
void iqr0();
void iqr1();
void iqr2();
void iqr3();
void iqr4();
void iqr5();
void iqr6();
void iqr7();
void iqr8();
void iqr9();
void iqr10();
void iqr11();
void iqr12();
void iqr13();
void iqr14();
void iqr15();
void (*iqrs[])() = {iqr0, iqr1, iqr2, iqr3, iqr4, iqr5, iqr6, iqr7,
iqr8, iqr9, iqr10, iqr11, iqr12, iqr13, iqr14, iqr15};
int i;
for (i=0; i<16; i++)
SETGATE(idt[IRQ_OFFSET+i], 0, GD_KT, iqrs[i], 0);
kern/env.c
的env_alloc()
添加
// Enable interrupts while in user mode.
// LAB 4: Your code here.
e->env_tf.tf_eflags |= FL_IF;
处理时钟中断
在 user/spin
程序中,子进程开启后就陷入死循环,此后 kernel 无法再获得控制权。我们需要让硬件周期性地产生时钟中断,强制将控制权交给 kernel,使得我们能够切换到其他进程。
调用JOS已经为我们写好了lapic_init
和pic_init
设置时钟和中断控制器产生中断,现在需要修改trap_dispatch()
,在时钟中断发生时调用sched_yield()
切换到其它进程
代码实现如下:
// Handle clock interrupts. Don't forget to acknowledge the
// interrupt using lapic_eoi() before calling the scheduler!
// LAB 4: Your code here.
if (tf->tf_trapno == IRQ_OFFSET+IRQ_TIMER) {
lapic_eoi();
sched_yield();
return;
}
这里注意trap_init
里,调用SETGATE
时第二个参数必须设置为0,即将它们都视为中断门,阻止中断嵌套(为了简化JOS内核逻辑)。如果它们设置为1,这次make grade
时会发现之前大量的测试用例都无法通过;所有视为陷阱门的中断,在触发时都会死在assert(!(read_eflags() & FL_IF));
这个断言。
这里感谢找不到工作:6.828 操作系统 lab4 实验报告:Part C指出的这个问题,最后跟询问了老师,理解了这些东西概念上的区别
具体的trap,interrupt,expection的区别,参考MIT-JOS系列:问题汇总(中断和异常概念整理)
进程间通信IPC
来自MIT-JOS官方:
(Technically in JOS this is “inter-environment communication” or “IEC”, but everyone else calls it IPC, so we’ll use the standard term.)
我们之前一直在专注于实现操作系统进程间的隔离,让每个进程认为自己“独占”一台机器而察觉不到其他进程的存在。操作系统的另一个重要功能是允许进程相互通信。这非常有用,pipe
就是进程间通信的一个典型例子
进程间通信有很多模型,在这里,我们将实现一个简单的IPC机制
JOS中的IPC
我们将为JOS增加系统调用sys_ipc_recv
和sys_ipc_try_send
实现JOS的IPC机制,并实现封装用户库函数ipc_recv
和ipc_send
在JOS的IPC机制中,用户进程间传递的消息(message)包括两部分:
- 一个32位的值
- 一个页面映射(可选)
允许进程通过页面映射传递消息提供了一种传输更多数据的方式,而且能使进程间能更方便地分析内存空间
发送和接受消息
进程调用sys_ipc_recv
接收消息。这个函数会挂起用户进程直到它收到一个消息。在它等待一个消息的时间里,任何一个其他进程都能向它发送一个消息。换句话说,我们在Part A中实现的权限检查并不适用于IPC。IPC的系统调用保证一个环境不会因为其他环境发送的消息而发生错误
进程调用sys_ipc_try_send
,以接受者的进程id和消息值作为参数,向指定进程发送消息。如果接受者正等待接收消息(已调用sys_ipc_recv
且还没收到消息),则发送者交付这个消息并返回0;否则发送者返回-E_IPC_NOT_RECV
表明目标进程没有接收消息
用户库中的ipc_recv
将负责调用sys_ipc_recv
接收一个消息,并从当前环境的struct Env
结构体中获取收到的消息值
用户库中的ipc_send
将反复调用sys_ipc_try_send
直到消息发送成功
页面传递
当进程调用sys_ipc_recv
并提供一个有效地址(UTOP
以下)dstva
作为参数时,表明它希望收到一个页面映射。如果发送者发送一个页面,这个页面会被映射到接受者的地址空间的dstva
处,之前位于 dstva
的页面映射会被覆盖
当进程调用sys_ipc_try_send
并提供一个有效地址(UTOP
以下)srcva
作为参数时,表明发送者希望发送映射在srcva
的页面给接受者,页面权限为perm
。当发送成功后,发送者保留其原有的页面映射,接收者将自己地址空间中的dstva
映射到发送者srcva
所指向的物理页,该物理页被两者共享
如果发送者和接收者之一没有明示想收到一个页面,则页面不会被传送。在IPC成功后,若有页面被传送,内核将接收者的Env
结构的env_ipc_perm
设置为页面的权限;如果没有页面被传送,env_ipc_perm
被设置为0
实现IPC
sys_ipc_recv
:挂起等待接收消息
- 判断是否要接收一个页面:
dstva < UTOP
- 如果
dstva < UTOP
但dstva
没有4K对齐,返回-E_INVAL
异常退出 - 如果
dstva >= UTOP
表明不想收到一个页面!!!
- 如果
- 设置当前进程的
env_ipc_recving
和env_ipc_dstva
- 挂起当前进程(
env_status
设置为ENV_NOT_RUNNABLE
),切换到其他环境运行 sys_ipc_recv
本身永不返回,但调用者能收到0作为正确返回值,这里和fork
原理一样,由sys_ipc_try_send
设置接收者寄存器eax
的值实现
代码实现如下:
static int
sys_ipc_recv(void *dstva)
{
// LAB 4: Your code here.
if ((uintptr_t)dstva < UTOP && ((uintptr_t)dstva & 0xFFF) != 0)
return -E_INVAL;
curenv->env_ipc_recving = 1;
curenv->env_ipc_dstva = dstva;
curenv->env_status = ENV_NOT_RUNNABLE;
sched_yield();
// panic("sys_ipc_recv not implemented");
return 0;
}
sys_ipc_try_send
:将消息值value
发送到id为envid
的进程
错误返回锦集:
-
如果判断发送一个页面:
srcva < UTOP
-
如果
srcva < UTOP
但srcva
没有4K对齐,返回-E_INVAL
-
如果
perm
权限不当:返回-E_INVAL
(参见sys_page_alloc
的页面权限)PTE_U | PTE_P must be set, PTE_AVAIL | PTE_W may or may not be set, but no other bits may be set
-
如果发送者的
srcva
没有映射任何物理页面,返回-E_INVAL
-
如果发送者指出的
perm
包括写权限但srcva
对应的页面只读,返回-E_INVAL
-
如果接受者没有足够的空间映射
srcva
,返回-E_NO_MEM
-
-
如果接收者没有挂起并等待消息,发送失败,返回
-E_IPC_NOT_RECV
-
如果
envid
指向的环境不存在,返回-E_BAD_ENV
若发送成功:设置接收者Env
结构:
- 设置
env_ipc_recving
为0,阻止收到其他消息 - 设置
env_ipc_from
为发送者的envid
- 设置
env_ipc_value
为value
- 如果发送了页面,
env_ipc_perm
设置为perm
,为接收者映射页面 - 将接收者标记为
ENV_RUNNABLE
- 设置接收者寄存器
eax
的值为0,使接收者的调用的sys_ipc_recv
返回0
代码实现如下:
static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
// LAB 4: Your code here.
// panic("sys_ipc_try_send not implemented");
struct Env *recv = NULL;
if (envid2env(envid, &recv, 0) < 0) return -E_BAD_ENV;
if (!recv->env_ipc_recving) return -E_IPC_NOT_RECV;
if ((uintptr_t)srcva < UTOP) {
if ((uintptr_t)srcva & 0xFFF) return -E_INVAL;
pte_t *pte = NULL;
struct PageInfo *page = page_lookup(curenv->env_pgdir, srcva, &pte);
if (!page) return -E_INVAL;
if ((*pte & (PTE_U | PTE_P)) == 0) return -E_INVAL;
if ((*pte & ~(PTE_U | PTE_P | PTE_AVAIL | PTE_W)) != 0) return -E_INVAL;
if (!(*pte & PTE_W) && (perm & PTE_W)) return -E_INVAL;
if (page_insert(recv->env_pgdir, page, recv->env_ipc_dstva, perm) < 0)
return -E_NO_MEM;
recv->env_ipc_perm = perm;
}
recv->env_ipc_recving = 0;
recv->env_ipc_from = curenv->env_id;
recv->env_ipc_value = value;
recv->env_status = ENV_RUNNABLE;
// 让接收者调用的sys_ipc_recv返回0
recv->env_tf.tf_regs.reg_eax = 0;
return 0;
}
ipc_recv
:调用sys_ipc_recv
,接收一个IPC消息并返回
- 如果
pg
非空,则发送者发送的页面将被映射到地址pg
(由发送者的系统调用sys_ipc_try_send
完成) - 如果
from_env_store
非空,则将发送者的envid
保存在from_env_store
- 如果
perm_store
非空,则将来自发送者的页面权限保存在perm_store
(没有页面传输则为0)- 如果系统调用失败,则
perm_store
和from_env_store
为0
- 如果系统调用失败,则
- 返回值为发送者发的
value
- 以上内容都已经被发送者通过
sys_ipc_try_send
系统调用保存在接收者的Env
结构中,可以用thisenv
获取
代码实现如下:
int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
// LAB 4: Your code here.
// panic("ipc_recv not implemented");
int err;
if ((err=sys_ipc_recv(pg)) < 0) {
if (from_env_store) *from_env_store = 0;
if (perm_store) *perm_store = 0;
return err;
}
if (from_env_store)
*from_env_store = thisenv->env_ipc_from;
if (perm_store) {
if (pg) *perm_store = thisenv->env_ipc_perm;
else *perm_store = 0;
}
return thisenv->env_ipc_value;
}
ipc_send
:调用sys_ipc_try_send
,向进程toenv
传送消息
- 如果
pg
非空,发送pg
所在的物理页面,权限为perm
- 不断进行系统调用直到
sys_ipc_try_send
返回成功 - 除了
-E_IPC_NOT_RECV
以外的错误都将引发panic
- 如果
pg
为空,则向sys_ipc_try_send
传送一个大于UTOP
的参数 - 对CPU好点,使用
sys_yield()
代码实现如下:
void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
// LAB 4: Your code here.
int err;
void *srcva = (void*)UTOP+1;
if (pg) srcva = pg;
do {
err = sys_ipc_try_send(to_env, val, pg, perm);
if (err < 0 && err != -E_IPC_NOT_RECV)
panic("ipc_send error: %e", err);
sys_yield();
} while (err);
// panic("ipc_send not implemented");
}