Lv的杂货铺

我们都是阴沟里的虫子,但总还是得有人仰望星空

0%

Lab 4: Preemptive Multitasking

Part A: Multiprocessor Support and Cooperative Multitasking

在part A中,将要实现多处理器支持以及协同多任务处理功能。

在一个对称多处理器(symmetric multiprocessing, SMP)系统中,所有的核心都具有同等地位,能够访问内存和IO总线。但是在系统启动的过程中,它们还是有主次之分的,BSP(bootstrap processor)首先启动,用于进行一些设置,初始化工作,然后在操作系统启动后,BSP再将AP(application processors)唤醒。至于哪个处理器是BSP,则是由BIOS和硬件来决定的。

在SMP中,每一个处理器都有一个LAPIC单元,用于传递中断,同时处理器也可以读取LAPIC单元来获取这个CPU的标识符。通过LAPIC,我们可以确定当前的代码是运行在哪个CPU上的,BSP也可以唤醒某个AP令其执行某段代码;在part C中,还将要利用LAPIC来产生时钟中断以实现抢占式多任务处理。

LAPIC是以MMIO的方式映射到物理内存0xFE000000处的,所以需要将每个CPU的LAPIC的地址空间分别映射到虚拟地址空间的MMIOBASE~MMIOLIM处。

Exercise 1

在exercise 1中,需要完成mmio_map_region()函数。该函数用于将一个物理地址映射到指定的虚拟地址处。实现还是很简单的,注意MMIO虚拟地址空间不足时panic即可。

1
2
3
4
5
6
7
8
9
size_t len = (size % PGSIZE == 0) ? size : (size - size % PGSIZE + PGSIZE);

if(base + len > MMIOLIM)
panic("MMIO out of space!\n");

boot_map_region(kern_pgdir, base, len, pa, PTE_PCD|PTE_PWT|PTE_W);
void* result = (void*)base;
base += len;
return result;

在启动AP之前,BSP会首先收集当前SMP的信息,例如AP的数量,它们的ID等等,在收集了必要的信息后,就可以着手启动AP了。现在,操作系统已经初始化完成,所以,启动AP的动作也很简单了,将一段代码复制到0x7000 (MPENTRY_PADDR),然后BSP通过LAPIC唤醒AP执行这一段代码把AP的内部状态初始化。

Exercise 2

exercise 2要求修改之前的page_init()代码,避免将将物理地址0x7000 (MPENTRY_PADDR)的物理页加入free list中,以保证这片物理地址不会被虚拟内存分配掉。新的实现代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
size_t i;

pages[0].pp_ref = 1;


// base memory
for(i = 1; i < MPENTRY_PADDR/PGSIZE; i++)
{
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}

pages[i++].pp_ref = 1;

for (; i < npages_basemem; i++)
{
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}


// IO hole
for(; i < EXTPHYSMEM/PGSIZE; i++)
{
pages[i].pp_ref = 1;
}

// kern & kern_pgdir & pages
for(; i < ((uint32_t)boot_alloc(0)&0x0ffffff)/PGSIZE; i++)
pages[i].pp_ref = 1;

for(; i < npages; i++)
{
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}

Question 1

可以看到,AP的启动代码mpentry.S与BSP的启动代码boot.S十分相似,都是将寄存器复位,加载GDT表,修改CR0寄存器的字段开启分段,通过一个长跳转跳转进入保护模式,然后加载页目录表,开启分页,设定栈指针并通过一个间接跳转跳转到C代码。它们之间的区别就是,开始的两处地方都使用了MPBOOTPHYS宏来将符号的地址改变。这是因为mpentry.S是跟内核一起链接的,而内核的链接地址显然在地址空间的顶部,所以这些符号的地址并不正确,使用宏计算相对偏移并加上MPENTRY_PADDR这个偏移量,才是真实的物理地址,而boot.S是在boot文件夹的,它的链接地址是0x7c00,所以它就不需要使用宏来重新计算地址了。

在mpentry.S中,开启了分页之后,并没有一步到位加载kern_pgdir这个页目录表,而是先加载了最初的那个临时页表(把虚拟地址[04M),[KERNBASEKERNBASE+4M)映射到物理地址[0~4M)的那个),这是因为此时的EIP还位于低地址,如果直接加载kern_pgdir的话,这个页表是没有相应的映射的,所以会导致错误。只有加载临时页表,然后通过间接跳转跳转到mp_main()(它的链接地址必然是处于高位地址的),这时EIP处于高位地址了,才能够加载kern_pgdir

由于IDT TSS,以及各个段寄存器还未设定,而内核栈也是每个CPU分开的,所以在mp_main()中,需要对每个CPU进行初始化。

Exercise 3

exercise 3要求修改之前的mem_init()代码,为每个CPU分配一个独立的内核栈,而这些内核栈的已经通过percpu_kstacks给出。各个内核栈在虚拟地址空间的布局可以参考memlayout.h。代码如下:

1
2
3
4
5
for (size_t i = 0; i < NCPU; i++)
{
uint32_t kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP);
boot_map_region(kern_pgdir, kstacktop_i - KSTKSIZE, KSTKSIZE, (uint32_t)PADDR(percpu_kstacks[i]), PTE_P | PTE_W);
}

Exercise 4

exercise 3要求修改之前的trap_init_percpu()代码,因为在SMP中,每个CPU都有自己的TSS,所以也不要再使用先前的全局符号ts了,而要使用thiscpu->cpu_ts,此外还需要为每个CPU在GDT中为其创建相应的TSS描述符,最后加载GDT和TSS到相应的寄存器,代码如下:

1
2
3
4
5
6
7
8
9
10
thiscpu->cpu_ts.ts_ss0 = GD_KD;
thiscpu->cpu_ts.ts_esp0 = KSTACKTOP - cpunum() * (KSTKSIZE + KSTKGAP);
thiscpu->cpu_ts.ts_iomb = sizeof(struct Taskstate);

gdt[(GD_TSS0 >> 3) + cpunum()] = SEG16(STS_T32A, (uint32_t) &(thiscpu->cpu_ts),
sizeof(struct Taskstate) - 1, 0);
gdt[(GD_TSS0 >> 3) + cpunum()].sd_s = 0;

ltr(((GD_TSS0 >> 3) + cpunum()) << 3);
lidt(&idt_pd);

在开始执行用户任务前,首先要解决多个CPU争用内核代码的问题。在JOS中,使用自旋锁来解决这个问题。需要添加锁操作的有两种情况:

  • 在AP被唤醒的过程中:AP被唤醒进行一些列初始化操作之后,在最后,会调用sched_yield()来请求内核调度一个用户进程并执行,这个过程自然要修改一些内核数据结构,比如envs数组,所以,在i386_init()中BSP唤醒AP之前,需要获取锁,而在AP调用sched_yield()前,也需要获取锁;

  • 在AP被完全唤醒后,系统正常运行时:用户环境进入内核态后,获取锁,在离开内核态之前,归还锁。在一个锁被一个用户环境获取到之后,若同时有其他环境请求获取锁,那么对应的CPU就被阻塞在那里一直空转,直到锁被归还。

Exercise 5

这个比较简单,在i386_init()中调用boot_aps()之前、mp_main()中调用sched_yield()之前分别添加lock_kernel(),以及在trap()中的开头添加:

1
2
if ((tf->tf_cs & 3) == 3)
lock_kernel();

另外,在env_run()中调用env_pop_tf()前添加unlock_kernel()即可。

Question 2

对于这个问题,可以考虑一下这样一种场景:CPU0在运行用户进程A,CPU1运行用户进程B。在某一时刻,进程A产生来一个异常,进行一番操作,在内核栈中构造好trapframe后跳转进内核态的trap()中进行异常处理,这时,进程B也产生了一个异常。尽管在对于进程A,它在进入trap()后便以及将外部中断屏蔽了,但是这只能管住CPU0,而CPU1依然能产生异常。虽然在trap()的一开头就要求首先尝试获取锁,但是回想一下异常处理过程,CPU首先从IDT表中查相应的中断处理函数入口点,然后转到内核栈,在它上面构造一个trapframe,才跳转到trap()。那么,如果所有CPU共享一个内核栈的话,在尝试获取锁之前,内核栈就已经被进程B的trapframe破坏掉了。这就是需要每个CPU有自己的内核栈的原因。

接下来实现轮询调度。sched_yield()的功能是寻找一个可以运行的用户环境,然后在当前的CPU上运行它;此外,还要实现一个系统调用sys_yield(),使得用户进程可以主动向内核交出系统资源,请求调度别的用户进程并运行。

sched_yield()要添加的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	// LAB 4: Your code here.

if(!thiscpu->cpu_env)
{
for(int i = 0; i < NENV; i++)
{
if(envs[i].env_status == ENV_RUNNABLE)
env_run(&envs[i]);
}
}
else
{
int index = thiscpu->cpu_env - envs;
for(int i = (index + 1) % NENV; i % NENV != index; i++)
{
if(envs[i % NENV].env_status == ENV_RUNNABLE)
env_run(envs + i % NENV);
}
if(envs[index].env_status == ENV_RUNNING)
env_run(envs + index);
}

此外,在syscall.c中的syscall()中,在switch语句中添加sys_sched()的case即可。

Question 3

在调用了lcr3()之后的瞬间,尽管当前的CR3寄存器的内容已经改变,变成了另一个环境的页目录表,但是对struct Env *e的引用却依然是有效的,这是因为e指向的是内核数据结构中的envs[]数组中的元素,而每一个环境的页目录表对应的内核地址空间的映射都是一样的(回想一下前面的env_setup_vm())。

Question 4

从一个环境切换到另一个环境有两种情况:

  • 用户进程中主动通过系统调用sys_sched()切换到另一个环境,而进行系统调用时,会产生一个int 48中断,并进入中断处理函数,会把此时的用户进程上下文存储在当前环境结构体的struct Trapframe env_tf字段,这样,以后就可以恢复寄存器状态;

  • 当前用户进程正在运行时,外部产生了一个时钟中断,同样的,会通过trapframe将进程上下文保存在当前环境结构体的struct Trapframe env_tf字段。

Exercise 6

exercise 6将要实现一些列系统调用,用于创建用户环境。

首先是sys_exofork(),它创建一个新的用户环境,除了寄存器状态与父进程相同外,其余都是空的,并且要求一次调用两次返回,就像Linux的fork()一样。其中一次调用两次返回可以通过修改新的环境结构体的env_tf.tf_regs.reg_eax来实现,因为在i386中,函数的返回值是装载到eax的,如果把trapframe中的eax值改为0,那么通过这个trapframe从内核态返回到用户态的时候,就相当于产生来一个返回值0了。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
	// LAB 4: Your code here.

struct Env * new_env, * parent_env;
envid2env(0, &parent_env, 1) ;
int r = env_alloc(&new_env, parent_env->env_id);
if(r < 0)
return r;

new_env->env_status = ENV_NOT_RUNNABLE;
new_env->env_tf = parent_env->env_tf;
new_env->env_tf.tf_regs.reg_eax = 0;
return new_env->env_id;

sys_env_set_status()sys_page_alloc()sys_page_map()sys_page_unmap()都比较简单,相对麻烦的是参数是否有效的判断,直接放代码:

sys_env_set_status()

1
2
3
4
5
6
7
8
9
10
	// LAB 4: Your code here.

struct Env * env;
int r = envid2env(envid, &env, 1);
if(r < 0)
return r;
if(!(status == ENV_RUNNABLE || status == ENV_NOT_RUNNABLE))
return -E_INVAL;
env->env_status = status;
return 0;

sys_page_alloc()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Env * env;
int r = envid2env(envid, &env, 1);
if(r < 0)
return r;

if((uint32_t)va >= UTOP || (uint32_t)va % PGSIZE !=0 ||
(!(perm & PTE_U)) || (!(perm & PTE_P)) ||
(perm & (~ PTE_SYSCALL)))
return -E_INVAL;

struct PageInfo * p = page_alloc(1);

if(!p)
return -E_NO_MEM;

return page_insert(env->env_pgdir, p, va, perm);

sys_page_map()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Env* src_env, * dst_env;
if(envid2env(srcenvid, &src_env, 1) < 0 || envid2env(dstenvid, &dst_env, 1) < 0)
return -E_BAD_ENV;

if((uint32_t)srcva >= UTOP || (uint32_t)srcva % PGSIZE !=0 ||
(uint32_t)dstva >= UTOP || (uint32_t)dstva % PGSIZE !=0 ||
!(page_lookup(src_env->env_pgdir, srcva, 0)) ||
(!(perm & PTE_U)) || (!(perm & PTE_P)) ||
(perm & (~ PTE_SYSCALL)) ||
((perm & PTE_W) && !((*pgdir_walk(src_env->env_pgdir, srcva, 0)) & PTE_W)))
return -E_INVAL;

struct PageInfo * p = page_lookup(src_env->env_pgdir, srcva, 0);
return page_insert(dst_env->env_pgdir, p, dstva, perm);

sys_page_unmap()

1
2
3
4
5
6
7
8
struct Env * env;
if(envid2env(0, &env, 1) < 0)
return -E_BAD_ENV;
if((uint32_t)va >= UTOP || (uint32_t)va % PGSIZE !=0)
return -E_INVAL;

page_remove(env->env_pgdir, va);
return 0;

至此,Part A部分就完成了。

Part B: Copy-on-Write Fork

part B将要实现写时复制的fork功能,首先要做的是完成用户级缺页异常处理。

在JOS的用户级缺页异常处理中,用户进程可以自己指定一个异常处理函数(当然这个函数也是在用户态下的),这点比较不同。整个异常处理的过程是:

  1. 用户进程产生一个缺页异常,进入内核的异常处理函数;
  2. 内核中的异常处理函数发现异常是发生在用户态的,那么,开始着手在用户异常栈(User Exception Stack)中构造一个UTrapframe帧,然后根据当前环境结构体中的env_pgfault_upcall字段得到下一步将要跳转到的代码(env_pgfault_upcall指向_pgfault_upcall,这是一段汇编代码,用于调用用户指定的缺页处理函数,处理完缺页异常后,返回,然后,再通过一些小技巧,完美地跳回原用户进程之前执行的地方);
  3. 将当前环境结构体中的trapframe的EIP,ESP分别调整为env_pgfault_upcall的地址以及UTrapframe帧的地址,然后通过env_run()跳转到用户级缺页异常处理代码处(此时,从内核态跳转到用户态)
  4. 进行缺页异常处理,并跳回原用户进程先前执行到的地方。

Exercise 8

exercise 8要求实现一个系统调用sys_env_set_pgfault_upcall(),通过它,用户进程就可以自己设定自己的_pgfault_upcall。代码比较简单,为:

1
2
3
4
5
6
7
8
	// LAB 4: Your code here.

struct Env * env;
int r = envid2env(envid, &env, 1);
if(r < 0)
return r;
env->env_pgfault_upcall = func;
return 0;

Exercise 9

exercise 9要求实现page_fault_handler()。它在用户异常栈(User Exception Stack)中构造一个UTrapframe帧并跳转到_pgfault_upcall代码处。需要注意的地方是:

  1. 确认用户异常栈是否满了,如果满了,自然需要panic;

  2. 可能会出现异常嵌套的情况,在处理一个缺页异常的过程中又触发了一个新的缺页异常,这时用户异常栈显然会有多个帧。此时,需要注意,UTrapframe帧与帧之间需要空出32bit的空间,至于为什么,后面会讲。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
	// LAB 4: Your code here.

if(curenv->env_pgfault_upcall)
{
struct UTrapframe* base;
if(UXSTACKTOP - PGSIZE <= last_tf->tf_esp && last_tf->tf_esp <= UXSTACKTOP - 1)
base = (struct UTrapframe*)(last_tf->tf_esp - 4 - sizeof(struct UTrapframe));
else
base = (struct UTrapframe*)(UXSTACKTOP - sizeof(struct UTrapframe));

user_mem_assert(curenv, base, PGSIZE, PTE_W);

base->utf_fault_va = fault_va;
base->utf_err = last_tf->tf_err;
base->utf_regs = last_tf->tf_regs;
base->utf_eip = last_tf->tf_eip;
base->utf_eflags = last_tf->tf_eflags;
base->utf_esp = last_tf->tf_esp;

curenv->env_tf.tf_eip = (uint32_t)curenv->env_pgfault_upcall;
curenv->env_tf.tf_esp = (uint32_t)base;

env_run(curenv);
}
// Destroy the environment that caused the fault.

Exercise 10

接下来要完成_pgfault_upcall的汇编代码。主要工作部分就是需要完成从这里跳转到该次缺页异常发生时的状态(通用寄存器状态、栈指针等等)。这里就要用到上面的UTrapframe帧与帧之间空出的32bit空间了。我们把要返回的代码段的地址(EIP应该还原成的值)放置在这里,然后再恢复各个通用寄存器的值,将esp调整为指向先前的32bit空间,最后通过一个ret语句,同时恢复esp和eip。代码如下:

1
2
3
4
5
6
7
8
9
10
11
addl $8, %esp
mov 32(%esp),%eax
mov 40(%esp),%ebx
subl $4, %ebx
mov %eax, (%ebx)
subl $4, 40(%esp)
popal
addl $4, %esp
popfl
popl %esp
ret

其中需要注意的是,在将eflags还原之后,就不能再使用算术操作了,否则会改变eflags的值。

Exercise 11

最后,需要完成set_pgfault_handler()。它是用于提供给用户进程的接口,用于给自己设定缺页异常的处理函数。如果是初次调用的话,会首先分配一个页作为用户异常栈。代码比较简单,为:

1
2
3
4
5
6
	// LAB 4: Your code here.

r = sys_page_alloc(0, (void*)(UXSTACKTOP - PGSIZE), PTE_U | PTE_W | PTE_P);
if(r < 0)
panic("set pgfault handler error %e\n", r);
sys_env_set_pgfault_upcall(0, _pgfault_upcall);

接下来,将要实现写时复制的fork。写时复制技术就不再多讲了因为这是很基础的知识。

Exercise 12

exercise 12包含三个需要完成的函数,它们一起完成了写时分配的fork。

首先是pgfault()用于解决写时分配时的缺页异常。在一个用户环境产生了一个页故障后,首先会判断这个故障是否是因为对一个标记了写时复制的页进行写操作进行的。如果不是,则需要panic。然后,开始着手解决。它会请求分配一个新的物理页,将旧的页的内容复制到新的页,然后把旧的物理页在相应虚拟地址上的映射取消掉,将新的物理页映射到原来的虚拟地址。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	// LAB 4: Your code here.

if(! ((err & 2) && ((((pte_t*)UVPT)[(uint32_t)addr/PGSIZE]) & PTE_COW)))
panic("error in function pgfault\n");

// Allocate a new page, map it at a temporary location (PFTEMP),
// copy the data from the old page to the new page, then move the new
// page to the old page's address.
// Hint:
// You should make three system calls.

// LAB 4: Your code here.

uint32_t old_page = (uint32_t)addr / PGSIZE;
sys_page_alloc(0, PFTEMP, PTE_W | PTE_U | PTE_P);
memmove(PFTEMP, (void*)(old_page * PGSIZE), PGSIZE);
sys_page_map(0, PFTEMP, 0, (void*)(old_page * PGSIZE), PTE_W | PTE_U | PTE_P);
sys_page_unmap(0, PFTEMP);

然后是duppage(),该函数是在fork()中使用到的,它的作用是将一个环境中的一个页映射到另一个环境的相同地址,只修改页表、页目录表,而不实际复制页中的内容。在映射一个可写的页面或者标记了写时复制的页面时,它会在目标环境中也将这个页标记为写时复制,代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	// LAB 4: Your code here.

pte_t src_pte = uvpt[pn];
if(src_pte)
{
if((src_pte & PTE_W) || (src_pte & PTE_COW))
{
if( (sys_page_map(0, (void*)(pn * PGSIZE), envid, (void*)(pn * PGSIZE), ((src_pte & 0xfff & ~PTE_W) | PTE_COW) & PTE_SYSCALL) < 0) ||
(sys_page_map(0, (void*)(pn * PGSIZE), 0, (void*)(pn * PGSIZE), ((src_pte & 0xfff & ~PTE_W) | PTE_COW) & PTE_SYSCALL) < 0) )
panic("duppage error on page map COW\n");
}
else
{
if(sys_page_map(0, (void*)(pn * PGSIZE), envid, (void*)(pn * PGSIZE), src_pte & 0xfff) < 0)
panic("duppage error on page map\n");
}
}

return 0;

其中,需要注意的两点是:

  • 需要先映射子进程的页为COW,然后再映射父进程的页为COW。原因是,如果先映射父进程,考虑一下这种情况:在执行完第一个sys_page_map() 之后,在执行第二个的时候,父进程(也是当前正在执行的进程)产生了一个对这个页的写操作,那么根据处理逻辑,这个页会被替换成一个新的物理页并清除PTE_COW标记。而执行完第二个sys_page_map()之后,子进程的这个页被标记为PTE_COW,这产生了父子进程之间的不统一:父进程可以随意对这个页写入,而子进程并不知情!

  • 另外,需要注意的是,虽然上面的代码中没有体现出来,但是在对子进程的页映射为PTE_COW之后,即使父进程相应的页之前也是PTE_COW,父进程也需要重新映射一遍,原因还是类似,考虑一下这种情况:如果在执行sys_page_map()中子进程建立相应的映射的瞬间之前,父进程产生了一个对这个页的写操作,那么根据处理逻辑,这个页会被替换成一个新的物理页并清除PTE_COW标记。同样,子进程被标记为PTE_COW而父进程没有这个标记,但是父子进程指向了相同的物理页,也产生了父子进程之间的不统一:父进程可以随意对这个页写入,而子进程不知情!

最后是fork()。它的代码的逻辑十分清晰,就不过多解释了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    // LAB 4: Your code here.
envid_t envid;
uint8_t *addr;
int r;
extern unsigned char end[];
set_pgfault_handler(pgfault);

envid = sys_exofork();
if (envid < 0)
panic("sys_exofork: %e", envid);
if(envid == 0)
{
thisenv = &envs[ENVX(sys_getenvid())];
return 0;
}

for(uint32_t i = 0; i < USTACKTOP / PGSIZE; i++)
{
if(uvpd[i / NPTENTRIES])
duppage(envid, i);
}

sys_page_alloc(envid, (void*)(UXSTACKTOP - PGSIZE), PTE_U | PTE_P | PTE_W);

sys_env_set_pgfault_upcall(envid, thisenv->env_pgfault_upcall);

if ((r = sys_env_set_status(envid, ENV_RUNNABLE)) < 0)
panic("sys_env_set_status: %e", r);

return envid;

至此,part B就完成了。

Part C: Preemptive Multitasking and Inter-Process communication (IPC)

part C主要的工作是两个部分:抢占式多任务处理,以及IPC。

在JOS中,抢占式多任务实现得比较简单,当时钟中断到来的时候,调用sched_yield()进行任务调度就行了。另外,在每个环境创建的时候,都要保证它是允许接收外部中断,即置eflags的FL_IF标志。

Exercise 13

这个exercise中要做的工作就是上面所说的了,比较简单

trapentry.S添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TRAPHANDLER_NOEC(handler32, 32)
TRAPHANDLER_NOEC(handler33, 33)
TRAPHANDLER_NOEC(handler34, 34)
TRAPHANDLER_NOEC(handler35, 35)
TRAPHANDLER_NOEC(handler36, 36)
TRAPHANDLER_NOEC(handler37, 37)
TRAPHANDLER_NOEC(handler38, 38)
TRAPHANDLER_NOEC(handler39, 39)
TRAPHANDLER_NOEC(handler40, 40)
TRAPHANDLER_NOEC(handler41, 41)
TRAPHANDLER_NOEC(handler42, 42)
TRAPHANDLER_NOEC(handler43, 43)
TRAPHANDLER_NOEC(handler44, 44)
TRAPHANDLER_NOEC(handler45, 45)
TRAPHANDLER_NOEC(handler46, 46)
TRAPHANDLER_NOEC(handler47, 47)

trap.c添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SETGATE(idt[32], 0, GD_KT, &handler32, 0)
SETGATE(idt[33], 0, GD_KT, &handler33, 0)
SETGATE(idt[34], 0, GD_KT, &handler34, 0)
SETGATE(idt[35], 0, GD_KT, &handler35, 0)
SETGATE(idt[36], 0, GD_KT, &handler36, 0)
SETGATE(idt[37], 0, GD_KT, &handler37, 0)
SETGATE(idt[38], 0, GD_KT, &handler38, 0)
SETGATE(idt[39], 0, GD_KT, &handler39, 0)
SETGATE(idt[40], 0, GD_KT, &handler40, 0)
SETGATE(idt[41], 0, GD_KT, &handler41, 0)
SETGATE(idt[42], 0, GD_KT, &handler42, 0)
SETGATE(idt[43], 0, GD_KT, &handler43, 0)
SETGATE(idt[44], 0, GD_KT, &handler44, 0)
SETGATE(idt[45], 0, GD_KT, &handler45, 0)
SETGATE(idt[46], 0, GD_KT, &handler46, 0)
SETGATE(idt[47], 0, GD_KT, &handler47, 0)

然后,在env.c的env_alloc()中相应地方添加:

1
2
// LAB 4: Your code here.
e->env_tf.tf_eflags |= FL_IF;

最后,不要忘记取消sched_halt()sti那一行的注释。

Exercise 14

exercise 14要进行时钟中断的处理。在时钟中断到来的时候,进行任务调度,代码如下:

1
2
3
4
5
6
7
8
	// 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();
}

即,如果中断号是时钟中断对应的编号的话,首先,调用lapic_eoi()写lapic中的eoi寄存器。在i386中,中断被处理完以后,需要向eoi寄存器通知中断已经被处理过了。接着,调用sched_yield()进行任务调度。

这个lab的最后,就是实现IPC了。IPC可以在进程间传递一个32bit的信息,以及一个可选的页面映射。需要实现两个IPC系统调用,以及两个封装这两个系统调用的库函数。

sys_ipc_recv()这个系统调用用于请求接收一个IPC信息。进程调用这个函数后,会修改env结构体的相应字段表示这个进程期望收到一个IPC,并记录下希望得到的页面映射的地址,然后,将这个环境标记为ENV_NOT_RUNNABLE,阻塞在这里,直到收到了一个IPC为止。

sys_ipc_try_send()则是向指定的进程号发送一个IPC信息,另外如果指定的进程想要收到一个页面映射,而发送方也想要发送一个页面映射,那么,就将一个页面映射传递过去,之后,发送方通过修改接收方的env结构体的字段,标记接收方不再可接收IPC信息,并将接受方进程标记为ENV_RUNNABLE,并重新运行被阻塞的接受方进程。

代码如下:

sys_ipc_recv()

1
2
3
4
5
6
7
8
9
10
11
    // LAB 4: Your code here.

if((uint32_t)dstva < UTOP && (uint32_t)dstva % PGSIZE !=0)
return -E_INVAL;

curenv->env_ipc_recving = 1;
curenv->env_ipc_dstva = dstva;
curenv->env_status = ENV_NOT_RUNNABLE;
sched_yield();

return 0;

sys_ipc_try_send()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    // LAB 4: Your code here.

struct Env * env_recv;
if(envid2env(envid, &env_recv, 0) < 0)
return -E_BAD_ENV;

if(!env_recv->env_ipc_recving)
return -E_IPC_NOT_RECV;

if((uint32_t)srcva < UTOP &&
((uint32_t)srcva % PGSIZE !=0 ||
!(page_lookup(curenv->env_pgdir, srcva, 0)) ||
(!(perm & PTE_U)) || (!(perm & PTE_P)) ||
(perm & (~ PTE_SYSCALL)) ||
((perm & PTE_W) && !((*pgdir_walk(curenv->env_pgdir, srcva, 0)) & PTE_W))))
return -E_INVAL;

if((uint32_t)srcva < UTOP && (uint32_t)env_recv->env_ipc_dstva < UTOP)
{
struct PageInfo * p = page_lookup(curenv->env_pgdir, srcva, 0);
int r = page_insert(env_recv->env_pgdir, p, env_recv->env_ipc_dstva, perm);
if(r < 0)
return r;
else
env_recv->env_ipc_perm = perm;
}
else
env_recv->env_ipc_perm = 0;

env_recv->env_ipc_recving = 0;
env_recv->env_ipc_from = sys_getenvid();
env_recv->env_ipc_value = value;
env_recv->env_status = ENV_RUNNABLE;
env_recv->env_tf.tf_regs.reg_eax = 0;
return 0;

另外,还有两个基于这两个系统调用的包装函数ipc_send()ipc_recv(),这两个包装函数就比较简单了,照着注释的要求完成即可,代码如下:

ipc_send()

1
2
3
4
5
6
7
8
9
    // LAB 4: Your code here.

int r;
while((r = sys_ipc_try_send(to_env, val, (!pg) ? (void*)0xffffffff : pg, perm)) < 0)
{
if(r != -E_IPC_NOT_RECV)
panic("ipc send error %e\n",r);
sys_yield();
}

ipc_recv()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	// LAB 4: Your code here.

int r = sys_ipc_recv((!pg) ? (void*)0xffffffff : pg);
if(r < 0)
{
if(from_env_store)
*from_env_store = 0;
if(perm_store)
*perm_store = 0;
return r;
}

if(from_env_store)
*from_env_store = thisenv->env_ipc_from;

if(perm_store)
*perm_store = thisenv->env_ipc_perm;

return thisenv->env_ipc_value;

至此,lab4就完成了。运行一下评分脚本
img

lab的完整代码在我的GitHub中可以找到

(完)