MIT-JOS系列10:多任务处理(三)

Part C:抢占式多任务处理和进程间通信(IPC)

注:根据MIT-JOS的lab指导手册,以下不明确区分“环境”和“进程”

重要提醒:每次实现完系统调用,记得补充kern/syscall.csyscall()!!!!!!!

在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.Skern/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.ctrap_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.cenv_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_initpic_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_recvsys_ipc_try_send实现JOS的IPC机制,并实现封装用户库函数ipc_recvipc_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 < UTOPdstva没有4K对齐,返回-E_INVAL异常退出
    • 如果dstva >= UTOP表明不想收到一个页面!!!
  • 设置当前进程的env_ipc_recvingenv_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 < UTOPsrcva没有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_valuevalue
  • 如果发送了页面,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_storefrom_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");
}
posted @ 2019-04-18 14:30  sssaltyfish  阅读(276)  评论(0编辑  收藏  举报