实验环境
Hardware:
Memory: 16G
Processor: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz × 6
OS Type: 64 bit
Disk: 512GB
Software
OS: Ubuntu 18.04 LTS(x86_64)
GCC: gcc 7.5.0 #gcc -v
Make: GNU Make 4.1 #make --verison
GDB: GNU gdb 8.1.0 #gdb --version
这个lab中要求在多个同时活跃的用户环境之间实现抢占式多任务调度。
Part A要求添加JOS对多处理的机制,实现Round-Robin调度,并且添加基本的环境管理系统调度。
Part B要求实现类UNIX的fork()
,允许用户态环境创建自己的拷贝。
Part C要求实现对进程间通信的支持,允许不同的用户环境之间显式地进行通信和同步。同时也需要添加对硬件时钟中断和抢占地支持。
准备工作
运行一下命令:
git pull
git checkout -b lab4 origin/lab4
git merge lab3
Part A: Multiprocessor Support and Cooperative Multitasking
在这部分中,需要把JOS扩展为多处理器系统,然后是一些新的系统调用允许用户态环境创建新的环境。同时需要实现合作式的Round-Robin调度。当当前用户环境自愿释放CPU的时候,允许kernel从当前用户环境切换到另一个用户环境。
多处理器支持
我们将让JOS支持“对称多处理”(SMP),其中所有的CPU都有同等的访问诸如内存和IO总线等系统资源的权限。尽管所有的处理器在SMP中都是一样的,在启动过程中,他们可以被分为两类:引导处理器(BSP)负责初始化系统并启动操作系统;应用处理器在操作系统被启动并在运行之后,被BSP唤醒。哪个处理器作为BSP是由BIOS决定的。到目前为止,所有的JOS代码都运行在BSP上。
在一个SMP系统中,所有的CPU都有相伴的本地APIC(LAPIC)单元。LAPIC单元负责在系统中发送中断。LAPIC同时提供给相连的CPU一个独特的标识符。在这个lab中需要利用LAPIC提供的以下基本功能(kern/lapic.c
):
- 读出LAPIC标识符(APIC ID)来获得我们代码当前运行在哪个CPU上(
cpunum()
) - 从BSP向AP发送
STARTUP
跨处理器中断来启动其它CPU(lapic_startap()
) - 在part c中,为LAPIC的内嵌计时器编程来引发时钟中断,支持抢占式调度(
apic_init()
)
处理器通过内存映射IO(MMIO)来访问LAPIC。在MMIO中一部分物理内存和IO设备的寄存器以硬连线的方式相连,所以同样的访问内存的load/store指令可以用来访问外设的寄存器。之前已经看到过从物理地址0xA0000
开始的一个IO hole,我们之前用它来写VGA. LAPIC的IO hole开始于虚拟地址0xFE000000
,它的物理地址在mpconfig.c
中的mp_init()
获得。 JOS虚拟内存映射中在MMIOBASE
留下了4MB的间距让我们来映射设备。
Exercise 1 实现kern/pmap.c
中的mmio_map_region
他的使用方法可以看kern/lapic.c
中的lapic_init
//
// Reserve size bytes in the MMIO region and map [pa,pa+size) at this
// location. Return the base of the reserved region. size does *not*
// have to be multiple of PGSIZE.
//
void *
mmio_map_region(physaddr_t pa, size_t size)
{
// Where to start the next region. Initially, this is the
// beginning of the MMIO region. Because this is static, its
// value will be preserved between calls to mmio_map_region
// (just like nextfree in boot_alloc).
static uintptr_t base = MMIOBASE;
// Reserve size bytes of virtual memory starting at base and
// map physical pages [pa,pa+size) to virtual addresses
// [base,base+size). Since this is device memory and not
// regular DRAM, you'll have to tell the CPU that it isn't
// safe to cache access to this memory. Luckily, the page
// tables provide bits for this purpose; simply create the
// mapping with PTE_PCD|PTE_PWT (cache-disable and
// write-through) in addition to PTE_W. (If you're interested
// in more details on this, see section 10.5 of IA32 volume
// 3A.)
//
// Be sure to round size up to a multiple of PGSIZE and to
// handle if this reservation would overflow MMIOLIM (it's
// okay to simply panic if this happens).
//
// Hint: The staff solution uses boot_map_region.
//
// Your code here:
size_t up = ROUNDUP(pa+size, PGSIZE);
size_t down = ROUNDDOWN(pa,PGSIZE);
size = up - down;
if (base+size >= MMIOLIM) panic("mmio overflow!\n");
boot_map_region(kern_pgdir,base,size, down,PTE_PCD|PTE_PWT|PTE_W);
base += size;
return (void *)(base - size);
}
这一部分按照注释,注意返回值,overflow和页对齐就行了。
应用处理器启动
在启动AP之前,BSP首先需要收集关于多处理器系统的信息,比如CPU总数,他们的APIC ID和各自LAPIC单元的地址。kern/mpconfig.c
中的mp_init()
通过读存储在BIOS中的MP配置表获得这个信息。
kern/init.c
中的boot_aps()
函数引导了AP启动过程。AP从实模式启动,很像bootloader在boot/boot.S
中启动。因此boot_aps()
复制了kern/mpentry.S
中的AP进入代码到一个可以在实模式下寻址的位置。和bootloader不同的是,我们对AP开始执行代码的位置有一定的控制。我们把进入代码复制到0x7000
(MPENTRY_PADDR
),但是认为低于640KB的未被使用的,页对齐的物理地址实际上都可以。
在那之后,boot_aps()
一个接着一个唤醒AP,通过发送给相应AP的LAPIC跨处理器中断STARTUP
,以及AP应当开始运行进入代码的CS:IP
地址(MPENTRY_PADDR
)。kern/mpentry.S
中的代码十分类似boot/boot.S
中的代码。在一些简单的初始化之后,它启动页表机制,让AP进入保护模式,然后调用C初始化例程mp_main()
(同样在kern/init.c
之中). boot_aps()
等待AP发送一个在它struct CpuInfo
中的cpu_status
域里的CPU_STARTED
信号,然后再唤醒下一个AP.
Exercise 2 要求阅读kern/init.c
中的boot_aps()
和mp_main()
以及kern/mpentry.S
中的汇编代码。确保明白了AP启动过程中的控制转移。然后修改page_init()
中的实现避免把从MPENTRY_PADDR
开始的页加入到空闲页列表中,让我们可以安全地在那个虚拟地址拷贝和运行AP启动代码。需要通过check_page_free_list()
测试
在page_init()
加上一个特判即可:
void
page_init(void)
{
// LAB 4:
// Change your code to mark the physical page at MPENTRY_PADDR
// as in use
// The example code here marks all physical pages as free.
// However this is not truly the case. What memory is free?
// 1) Mark physical page 0 as in use.
// This way we preserve the real-mode IDT and BIOS structures
// in case we ever need them. (Currently we don't, but...)
// 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)
// is free.
// 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must
// never be allocated.
// 4) Then extended memory [EXTPHYSMEM, ...).
// Some of it is in use, some is free. Where is the kernel
// in physical memory? Which pages are already in use for
// page tables and other data structures?
//
// Change the code to reflect this.
// NB: DO NOT actually touch the physical memory corresponding to
// free pages!
size_t i;
size_t IO_Hole_start = (size_t)IOPHYSMEM / PGSIZE;
size_t IO_Hole_end = (size_t)EXTPHYSMEM / PGSIZE;
size_t EXT_Used_end = PADDR(boot_alloc(0)) / PGSIZE;
//Mark page 0 as used
pages[0].pp_ref = 1;
pages[0].pp_link = NULL;
for (i = 1; i < npages; i++) {
if ((i>= IO_Hole_start && i<EXT_Used_end)||( i == MPENTRY_PADDR/PGSIZE)){
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
else
{
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}
}
Question
- 按行比对
kern/mpentry.S
和boot/boot.S
,记得kern/mpentry.S
和kernel中其它的东西一样,也是编译链接运行在KERNBASE
之上。其中宏MPBOOTPHYS
的作用是什么?为什么它在kern/mpentry.S
中需要而在boot/boot.S
中不需要?换言之如果它在kern/mpentry.S
中被省略了,什么地方可能会发生错误?
MPBOOTPHYS
是把高地址计算成低地址。
因为kern/mpentry.S
被链接到了高地址,而实际上被加载在低地址上(0x7000
),而mpentry.S
运行在实模式下,需要把高地址转换为低地址。而boot.S
被同时加载和连接到低地址,于是运行在实模式不需要转换。
boot_aps()
, mp_main()
和mpentry.S
之间的关系后面再说。
每个CPU的状态和初始化
当写一个多处理器操作系统的时候,很重要的是区分每个CPU私有的状态和整个操作系统共享的状态。kern/cpu.h
中定义了中大部分的CPU私有状态,包括struct CpuInfo
结构,其中保存了每个CPU的变量。cpunum()
总是返回调用它的CPU的ID,可以被用来做cpus
的下标。宏thiscpu
可以方便地获得当前CPU的CpuInfo
以下是需要了解的每个CPU的私有状态:
- 内核栈
因为多个CPU可能同时陷入内核,我们需要给每个处理器一个不同内核栈来防止他们互相干涉对方的执行。percpu_kstacks[NCPU][KSTKSIZE]
为NCPU个内核栈留出了空间。
在lab2中,我们把bootstack
中指向的物理内存映射到KSTACKTOP
下面KSTKSIZE
的区域。类似的,这个lab中也要做类似的事情,需要把每个CPU的内核栈映射到这块区域,用保护页在栈之间隔离。CPU0的内核栈从KSTACKTOP
向下生长,CPU1从CP0的底部下面KSTKGAP
byte开始向下延申。看看inc/memlayout.h
会更清楚。 - TSS和TSS描述符
每个CPU也需要一个TSS来指明内核栈的位置。CPU i的TSS存储在cpus[i].cpu_ts
之中,相应的TSS描述符定义在GDT表项gdt[(GD_TSS0 >> 3) + i]
之中。全局变量ts
不再有用。 - 当前环境指针
因为同时每个CPU可以运行不同的用户环境,我们重新定义符号curenv
指向cpus[cpunum()].cpu_env
,指向当前CPU当前正在运行的环境。 - 系统寄存器
所有的寄存器都是对CPU私有的。因此,所有初始化寄存器的指令包括lcr3()
,ltr()
,lgdt()
,lidt()
等都必须在每个CPU上执行一次。env_init_percpu()
和trap_init_percpu()
函数就是用来实现这个目的的。
Exercise 3 要求修改mem_init_mp()
映射从KSTACKTOP
开始的每个CPU的内核栈。需要通过check_kern_pgdir()
中的新检查。
static void
mem_init_mp(void)
{
// Map per-CPU stacks starting at KSTACKTOP, for up to 'NCPU' CPUs.
//
// For CPU i, use the physical memory that 'percpu_kstacks[i]' refers
// to as its kernel stack. CPU i's kernel stack grows down from virtual
// address kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP), and is
// divided into two pieces, just like the single stack you set up in
// mem_init:
// * [kstacktop_i - KSTKSIZE, kstacktop_i)
// -- backed by physical memory
// * [kstacktop_i - (KSTKSIZE + KSTKGAP), kstacktop_i - KSTKSIZE)
// -- not backed; so if the kernel overflows its stack,
// it will fault rather than overwrite another CPU's stack.
// Known as a "guard page".
// Permissions: kernel RW, user NONE
//
// LAB 4: Your code here:
uint32_t i;
uintptr_t kstacktop_i;
for(i= 0 ;i<NCPU;i++){
kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP);
boot_map_region(kern_pgdir,kstacktop_i-KSTKSIZE,KSTKSIZE,PADDR(percpu_kstacks[i]),PTE_W);
}
}
Exercise 4 trap_init_percpu()
为BSP初始化了TSS和TSS描述符,修改它使得对所有CPU都能工作。
// Initialize and load the per-CPU TSS and IDT
void
trap_init_percpu(void)
{
// The example code here sets up the Task State Segment (TSS) and
// the TSS descriptor for CPU 0. But it is incorrect if we are
// running on other CPUs because each CPU has its own kernel stack.
// Fix the code so that it works for all CPUs.
//
// Hints:
// - The macro "thiscpu" always refers to the current CPU's
// struct CpuInfo;
// - The ID of the current CPU is given by cpunum() or
// thiscpu->cpu_id;
// - Use "thiscpu->cpu_ts" as the TSS for the current CPU,
// rather than the global "ts" variable;
// - Use gdt[(GD_TSS0 >> 3) + i] for CPU i's TSS descriptor;
// - You mapped the per-CPU kernel stacks in mem_init_mp()
// - Initialize cpu_ts.ts_iomb to prevent unauthorized environments
// from doing IO (0 is not the correct value!)
//
// ltr sets a 'busy' flag in the TSS selector, so if you
// accidentally load the same TSS on more than one CPU, you'll
// get a triple fault. If you set up an individual CPU's TSS
// wrong, you may not get a fault until you try to return from
// user space on that CPU.
//
// LAB 4: Your code here:
struct Taskstate* this_ts = &(thiscpu->cpu_ts);
this_ts->ts_esp0 = KSTACKTOP - thiscpu->cpu_id * (KSTKGAP + KSTKSIZE);
this_ts->ts_ss0 = GD_KD;
this_ts->ts_iomb = sizeof(struct Taskstate);
gdt[(GD_TSS0 >> 3) + thiscpu->cpu_id] = SEG16(STS_T32A, (uint32_t) (this_ts),
sizeof(struct Taskstate) - 1, 0);
gdt[(GD_TSS0 >> 3) + thiscpu->cpu_id].sd_s = 0;
ltr(GD_TSS0 + ((thiscpu->cpu_id)<<3));
lidt(&idt_pd);
}
运行make qemu CPUS=4
可以看到包括以下内容的输出
check_page_free_list() succeeded!
Fail to allocate a new page! Out of Free Physical Memory!
Fail to allocate a new page! Out of Free Physical Memory!
check_page_alloc() succeeded!
Fail to allocate a new page! Out of Free Physical Memory!
Fail to allocate a new page! Out of Free Physical Memory!
Fail to allocate a new page! Out of Free Physical Memory!
Fail to allocate a new page! Out of Free Physical Memory!
Fail to allocate a new page! Out of Free Physical Memory!
Fail to allocate a new page! Out of Free Physical Memory!
check_page() succeeded!
check_kern_pgdir() succeeded!
check_page_free_list() succeeded!
check_page_installed_pgdir() succeeded!
SMP: CPU 0 found 4 CPU(s)
enabled interrupts: 1 2
SMP: CPU 1 starting
SMP: CPU 2 starting
SMP: CPU 3 starting
重新稍微说一下之前的几个函数。mp_init()
函数从BIOS中读取CPU数量,CPU LAPIC ID以及LAPIC MMIO地址,初始化cpus数组,NCPU,和bootcpu.
boot_aps()
启动每个AP,每个AP启动之后执行mpentry.S
中的代码,跳转到mp_main
中,设置GDT,TSS等等,最后设置cpu_status,然后BSP就知道AP已经被启动了。
锁
我们现在的mp_main()
之中初始化了AP之后就开始循环。在我们让AP有更多进展之前,我们需要首先解决多个CPU同时运行kernel代码的竞争问题。最简单的解决方法是搞一个大的kernel锁。这个锁是单一的全局锁。不管哪个环境运行kernel代码就拿着这个锁,回到用户态之后释放掉这个锁。在这个模型下,用户环境可以在任意可用的CPU上并发运行,但是没有多于1个环境可以在内核态运行。任何尝试运行内核态的环境都必须等到拿到大的内核锁。
kern/spinlock.h
中声明了大的内核锁kernel_lock
,同时它还提供了lock_kernel
和unlock_kernel
,用来获得和释放锁的方法。需要在四个地方运用大的内核锁:
- 在
i386_init()
中,在BSP唤醒其它AP之前占有锁 - 在
mp_main()
中,在初始化AP之后获得锁,然后调用sched_yield
开始在这个AP上运行环境 - 在
trap()
中,当从用户态陷入的时候占有锁,,检查tf_cs
确定是从内核态还是用户态陷入。 - 在
env_run()
中,在恰好要切换回用户态之前释放锁,不要太晚或者太早做以免竞争或者死锁。
Exercise 5 要求在上面描述的地方加锁。
除了最后一个都是在注释下面加上lock_kernel()
最后一个:
unlock_kernel();
env_pop_tf(&e->env_tf);
Question
2. 看起来大的内核锁保证了一次只有一个CPU在运行内核代码,那为什么还需要给每个CPU一个单独的内核栈呢?描述即使有锁的保护,使用共享内核栈也会出错的场景。
在trap()
在内核栈中保存现场完了,即把用户态的寄存器都压入了内核栈之后,才获得了内核锁。如果用共享栈,可能另外一个CPU也压入了它的用户态寄存器,那么之前那个CPU保存在curenv中的tf可能就是错误的结果。
Round-Robin调度
下面需要修改JOS kernel以Round-Robin的方式在不同的环境之间切换。JOS中的Round-Robin调度工作方式如下:
kern/sched.c
中的sched_yield()
函数负责选择一个新的环境运行。它用循环的方式在envs[]
数组中从之前的运行环境开始顺序查找(如果没有的话就从头开始找)。找到第一个状态为ENV_RUNNABLE
的环境,然后跳转到那个环境开始运行。sched_yield()
永远不会同时在两个不同的CPU上运行同样的环境。它可以通过检查环境状态是不是ENV_RUNNING
来判断环境是不是在某个CPU上运行。- 一个新的系统调用函数
sys_yield()
可以被用户环境调用来调用内核的sched_yield()
函数,然后资源放弃当前CPU给另外一个环境。
Exercise 6 要求实现在sched_yield()
中按照上面说的实现Round-Robin调度,不要忘记修改syscall()
来分配sys_yield()
记得要在mp_main()
中调用sched_yield()
修改kern/init.c
创建三个或者更多环境,运行user/yield.c
程序
在kern/syscall.c
中增加:
int32_t
syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
// Call the function corresponding to the 'syscallno' parameter.
// Return any appropriate return value.
// LAB 3: Your code here.
switch (syscallno) {
case SYS_cgetc:
return sys_cgetc();
case SYS_cputs:
sys_cputs((const char*)a1, a2);
return 0;
case SYS_env_destroy:
return sys_env_destroy(a1);
case SYS_getenvid:
return sys_getenvid();
case SYS_yield:
sys_yield();
return 0;
default:
return -E_INVAL;
}
}
补全sched_yield
如下:
// Choose a user environment to run and run it.
void
sched_yield(void)
{
struct Env *idle;
// Implement simple round-robin scheduling.
//
// Search through 'envs' for an ENV_RUNNABLE environment in
// circular fashion starting just after the env this CPU was
// last running. Switch to the first such environment found.
//
// If no envs are runnable, but the environment previously
// running on this CPU is still ENV_RUNNING, it's okay to
// choose that environment.
//
// Never choose an environment that's currently running on
// another CPU (env_status == ENV_RUNNING). If there are
// no runnable environments, simply drop through to the code
// below to halt the cpu.
// LAB 4: Your code here.
int j;
int start = curenv ? ENVX(curenv->env_id)+1 : 0;
for (int i = 0; i<NENV; i++){
j = (start + i) % NENV;
if(envs[j].env_status == ENV_RUNNABLE){
env_run(&envs[j]);
}
}
if(curenv && curenv->env_status == ENV_RUNNING){
env_run(curenv);
}
// sched_halt never returns
sched_halt();
}
然后在i386_init()
中添加:
void
i386_init(void)
{
// Initialize the console.
// Can't call cprintf until after we do this!
cons_init();
//unsigned int i = 0x00646c72;
//cprintf("H%x Wo%s", 57616, &i);
//int x = 1, y = 3, z = 4;
//cprintf("x %d, y %x, z %d\n", x, y, z);
//cprintf("%C%s%C%s\n",0x0200, "green", 0x0400, "red");
cprintf("6828 decimal is %o octal!\n", 6828);
// Lab 2 memory management initialization functions
mem_init();
// Lab 3 user environment initialization functions
env_init();
trap_init();
// Lab 4 multiprocessor initialization functions
mp_init();
lapic_init();
// Lab 4 multitasking initialization functions
pic_init();
// Acquire the big kernel lock before waking up APs
// Your code here:
lock_kernel();
// Starting non-boot CPUs
boot_aps();
#if defined(TEST)
// Don't touch -- used by grading script!
ENV_CREATE(TEST, ENV_TYPE_USER);
#else
// Touch all you want.
//ENV_CREATE(user_primes, ENV_TYPE_USER);
#endif // TEST*
ENV_CREATE(user_yield,ENV_TYPE_USER);
ENV_CREATE(user_yield,ENV_TYPE_USER);
ENV_CREATE(user_yield,ENV_TYPE_USER);
// Schedule and run the first user environment!
sched_yield();
}
用两个CPU运行测试make qemu-nox CPUS=2
Question
3. 在env_run()
中会调用lcr3()
. 在调用lcr3()
之前和之后会访问同一个参数e。在加载%cr3
寄存器之后,地址上下文是会立刻变化的。但是虚拟地址的含义是和给定的地址上下文相联系的,即地址上下文明确了虚拟地址映射的物理地址。那么为什么指针e可以在地址上下文切换前后都能被解引用?
Answer: 因为之前在env_setup_vm
中把kern_pgdir
复制了一份,只修改一下页目录指向自己的那一项,因此struct Env
它作为内核数据结构的一部分,其虚拟地址以及虚拟地址到物理地址的映射都没有发生变化,在加载%cr3
寄存器前后是一样的。
4. 不管什么时候进行环境切换,都需要保证原来环境中的寄存器被妥善地保存了,使得他们以后可以恰当地恢复。为什么?这发生在哪里?
Answer: 因为可能之后需要回来原来的环境继续运行。保存发生在中断处理程序之中,可以看trapentry.S
,之后在trap()
中拷贝了一份到curenv->env_tf
之中。恢复的时候通过env_pop_tf()
弹出到寄存器。
创建新环境的系统调用
虽然现在kernel可以运行并且切换多个用户环境,但是这些环境还是只能由kernel最开始创建。现在需要实现必要的JOS系统调用,允许用户创建并运行其它的用户环境。
UNIX提供了fork
系统调用,它复制了调用进程(父进程)的整个地址空间来创建一个新的进程(子进程)。他们两者从用户来看的区别只有不一样的进程ID和父进程ID。在父进程中,fork
返回的是子进程ID,但是在子进程中,返回的是0.默认情况下,每个进程拥有自己的私有地址空间,并且两者的内存修改对对方都是不可见的。
需要实现一组不太一样的,更加原始的JOS系统调用来创建新的用户态环境。通过这些系统调用,可以在用户空间实现一个类UNIX的fork()
函数。需要写如下的系统调用:
sys_exofork
这个系统调用创建一个新的环境。它几乎是一片空白:用户地址空间没有映射并且它不可运行。新的用户环境回合调用sys_exofork
时的父进程有同样的寄存器状态。在父进程中,sys_exofork
返回的时新创建环境的envid_t
(如果创建失败则返回-1)。在子进程中,则会返回0。sys_env_set_status
设置指定环境的状态为ENV_RUNNABLE
或者ENV_NOT_RUNNABLE
。当某个环境环境的地址空间和寄存器状态被完全初始化好了之后,这个系统调用通常用来标志这个环境准备好运行了。sys_page_alloc
在给定环境的地址空间中,分配一页物理内存并且映射到制定的虚拟地址空间。sys_page_map
把一个环境地址空间的页映射(不是页内容)拷贝到另一个环境。恰当地设置内存共享使得新的和旧的映射可以引用同一页物理内存。sys_page_unmap
在给定环境中取消一个页的物理映射。
对于上面所有的接受环境的ID为参数的系统调用,JOS支持0作为当前环境。
测试程序user/dumbfork.c
中有一个非常原始的类UNIXfork
实现。测试程序使用上面的系统调用创新并运行一个子环境。然后两个环境使用前面的sys_yield
来互相切换。
Exercise 7 在kern/syscall.c
中实现上面的系统调用,并且确认syscall
调用了他们。将会使用到kern/pmap.c
和kern/env.c
中的很多函数,包括envid2env()
。到目前为止,调用envid2env
时都传入1作为checkperm
的参数。确保检查了所有非法的系统调用参数,返回-E_INVAL
如果碰到了非法。用user/dumbfork
测试。
sys_exofork
:
static envid_t
sys_exofork(void)
{
// Create the new environment with env_alloc(), from kern/env.c.
// It should be left as env_alloc created it, except that
// status is set to ENV_NOT_RUNNABLE, and the register set is copied
// from the current environment -- but tweaked so sys_exofork
// will appear to return 0.
// LAB 4: Your code here.
struct Env *e;
int ret = env_alloc(&e, curenv->env_id);
if(ret<0) return ret;
e->env_status = ENV_NOT_RUNNABLE;
e->env_tf = curenv->env_tf;
e->env_tf.tf_regs.reg_eax = 0;
return e->env_id;
}
这个部分的话因为异常的时候的返回值和env_alloc
中异常的情况是一致的,所以异常的时候只要返回env_alloc
的返回值就可以了。然后按照要求设置寄存器和状态。主要要把子进程的eax寄存器设为0,这样子进程返回的时候就能根据要求返回0了。
sys_env_set_status
static int
sys_env_set_status(envid_t envid, int status)
{
// Hint: Use the 'envid2env' function from kern/env.c to translate an
// envid to a struct Env.
// You should set envid2env's third argument to 1, which will
// check whether the current environment has permission to set
// envid's status.
// LAB 4: Your code here.
if(status != ENV_RUNNABLE && status != ENV_NOT_RUNNABLE){
return -E_INVAL;
}
struct Env *e;
if(envid2env(envid,&e,1)<0){
return -E_BAD_ENV;
}
e->env_status = status;
return 0;
}
按照异常值返回的要求判断一下就好。
sys_page_alloc
static int
sys_page_alloc(envid_t envid, void *va, int perm)
{
// Hint: This function is a wrapper around page_alloc() and
// page_insert() from kern/pmap.c.
// Most of the new code you write should be to check the
// parameters for correctness.
// If page_insert() fails, remember to free the page you
// allocated!
// LAB 4: Your code here.
struct Env *e;
int ret = envid2env(envid,&e,1);
if (ret<0) return ret;
if((va>= (void*)UTOP)||(ROUNDDOWN(va,PGSIZE)!=va)) return -E_INVAL;
int flag = PTE_U | PTE_P;
if(((perm& flag)!=flag)||((perm&~(flag|PTE_W|PTE_AVAIL))!=0)){
return -E_INVAL;
}
struct Page *pginfo = page_alloc(ALLOC_ZERO);
if(!pginfo) return -E_NO_MEM;
ret= page_insert(e->env_pgdir,pginfo,va,perm);
if(ret<0){
page_free(pginfo);
return ret;
}
return 0;
}
记得如果插入到页表里时候如果发生了错误要把分配的物理页给释放了。
sys_page_map
static int
sys_page_map(envid_t srcenvid, void *srcva,
envid_t dstenvid, void *dstva, int perm)
{
// Hint: This function is a wrapper around page_lookup() and
// page_insert() from kern/pmap.c.
// Again, most of the new code you write should be to check the
// parameters for correctness.
// Use the third argument to page_lookup() to
// check the current permissions on the page.
// LAB 4: Your code here.
struct Env *src_e, *dst_e;
if((srcva>= (void*)UTOP)||(PGOFF(srcva)!=0)) return -E_INVAL;
if((dstva>= (void*)UTOP)||(PGOFF(dstva)!=0)) return -E_INVAL;
int flag = PTE_U | PTE_P;
if(((perm& flag)!=flag)||((perm&~(flag|PTE_W|PTE_AVAIL))!=0)){
return -E_INVAL;
}
if(envid2env(srcenvid,&src_e,1)<0 ||envid2env(dstenvid,&dst_e,1)<0){
return -E_BAD_ENV;
}
pte_t *pte;
struct PageInfo *p = page_lookup(src_e->env_pgdir,srcva,&pte);
if(p == NULL) return -E_INVAL;
if((*pte&PTE_W)==0 && (perm & PTE_W)==1) return -E_INVAL;
if(page_insert(dst_e->env_pgdir,p,dstva,perm)<0) return -E_NO_MEM;
return 0;
}
主要是不要漏掉返回错误值的情况。
sys_page_unmap
static int
sys_page_unmap(envid_t envid, void *va)
{
// Hint: This function is a wrapper around page_remove().
// LAB 4: Your code here.
if((va>=(void*)UTOP)|| PGOFF(va)!= 0) return -E_INVAL;
struct Env *env;
int ret = envid2env(envid);
if (ret) return ret;
page_remove(env->env_pgdir, va);
return 0;
}
记得要改一下syscall
int32_t
syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
// Call the function corresponding to the 'syscallno' parameter.
// Return any appropriate return value.
// LAB 3: Your code here.
switch (syscallno) {
case SYS_cgetc:
return sys_cgetc();
case SYS_cputs:
sys_cputs((const char*)a1, a2);
return 0;
case SYS_env_destroy:
return sys_env_destroy(a1);
case SYS_getenvid:
return sys_getenvid();
case SYS_yield:
sys_yield();
return 0;
case SYS_exofork:
return sys_exofork();
case SYS_env_set_status:
return sys_env_set_status(a1,a2);
case SYS_page_alloc:
return sys_page_alloc(a1,(void *)a2,(int)a3);
case SYS_page_map:
return sys_page_map(a1,(void *)a2,a3, (void *)a4,(int)a5);
case SYS_page_unmap:
return sys_page_unmap(a1,(void *)a2);
default:
return -E_INVAL;
}
}
运行一下make run-dumpfork-nox
可以看到通过了。