为了方便查看最终源码,我将代码放到了我的github上。后面会把前面lab
的也会添加进去。lab4
有很多细节,所以有些东西我会分一下测试程序。
Lab 4: Preemptive Multitasking
- PartA:
为JOS
增添多处理器支持特性。
实现round-robin scheduling
循环调度。
添加一个基本的环境(进程)管理系统调用(创建和销毁环境,分配和映射内存)。 - PartB:
实现一个类Unix
的fork()
,其允许一个用户模式的环境能创建一份它自身的拷贝。 PartC:
支持进程间通信(
inter-process communication
,IPC
)
支持硬件时钟中断和抢占
做个大致介绍让你明白要做啥。
然后就让你切换到lab4
,每个lab
必须做的事情,然后会多出来一些文件。每个文件的作用看翻译应该就能明白,在我的github
每个文件最前面也有注释。
Part A: Multiprocessor Support and Cooperative Multitasking
先是一堆介绍,就是告诉你要实现轮转调度。后面会为你实现抢占式调度。还有要多CPU
支持。
Multiprocessor Support
我们将让 JOS 支持对称多处理器(symmetric multiprocessing
,SMP
),具体是什么东西自己去看讲操作系统的书。CPU功能基本都是一样的,但是在引导过程中可以分为两类:
- 引导处理器(BSP):负责初始化系统和引导操作系统;
- 应用程序处理器(AP):只有在操作系统启动并运行后,BSP才会激活应用程序处理器。
在我们前面所做过的所有实验都是在BSP
上面,现在我们要做的就是在BSP
上启动AP
。对于哪一个CPU是BSP
是硬件决定的。
每个CPU都有自己的APIC,也就是LAPIC
。APIC 一句话来说就是可编程中断。
- 根据
LAPIC
识别码(APIC ID)
区别我们的代码运行在哪个CPU上。(cpunum()
) - 从BSP向APs发送
STARTUP
处理器间中断(IPI
)去唤醒其他的CPU。(lapic_startap()
) - 在
Part C
,我们编写LAPIC的内置定时器来触发时钟中断,以支持抢占式多任务(pic_init()
)。
对于这些我们来看看这个文件kern/lapic.c
,一如既往,我们不用知道具体实现,知道一些重要的东西就行。
lapic.c
1 | // The local APIC manages internal (non-I/O) interrupts. |
看了上面的,其实啥都不知道,就耽误了下时间。。。。LAPIC
的 hole
开始于物理地址0xFE000000
(4GB
之下的32MB
),但是这地址太高我们无法访问通过过去的直接映射(虚拟地址0xF0000000
映射0x0
,即只有256MB
)。但是JOS
虚拟地址映射预留了4MB
空间在MMIOBASE
处,我们需要分配映射空间。练习 1
要我们实现 kern/pmap.c
里的mmio_map_region
,刚才我们上面那个文件有一句lapic = mmio_map_region(lapicaddr, 4096);
。和我们实现过的boot_map_region
很像,照着来就行了。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
40//
// 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.
// 映射 size 大小的 空间,必须页对齐。
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). //这个和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 映射pa 到base
// [base,base+size). Since this is device memory and not 因为不是DRAM 内存
// regular DRAM, you'll have to tell the CPU that it isn't 你不许告诉CPU是不安全的去高速缓存直接访问这个内存。
// safe to cache access to this memory. Luckily, the page 幸运的是 页表提供这种模板
// tables provide bits for this purpose; simply create the 简单的用了两个标志位PTE_PCD|PTE_PWT
// 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 //确保也对其 没有溢出 MMIOLIM 不然就是 panic
// 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 = ROUNDUP(size, PGSIZE);//页对齐然后映射 后面这个标志位,就是前面设定的
//个人感觉如果这个地方溢出了应该要判断一下,但是好像并没有这个测试所以好像没啥问题。
boot_map_region(kern_pgdir, base, size, pa, PTE_W | PTE_PWT | PTE_PCD);
base += size;
return (void *)base-size;
//github 上面和这个不一样但是差距也不大。
//panic("mmio_map_region not implemented");
}
Application Processor Bootstrap
在我们启动APs之前BSP需要收集他们的信息。比如数量,APICID 和他们映射的地址。kern/mpconfig.c
里面的mp_init
就是干这个的。我们去看看他在哪调用的,这些东西肯定是在初始化函数里面。
i386_init
1 | void |
看完后我们去看看mp_init()
。并没有要我们实现,过一眼就行了,由于注释太少,所以没看懂。知道他把所有CPU信息收集完就行了。boot_aps()
是引导其他CPU启动的。他和bootloader
差不多。所以他也是从实模式开始的。我们将kern/mpentry.S
加载到0x7000
,很眼熟…,好像只要是没有用的页对齐的低于640的地址都可以。
然后欧美就去看看他做了啥。
boot_aps
1 | // Start the non-boot (AP) processors. |
然后我们去看看mp_main()
在哪进入的。我们能够看出boot_aps
,让我慢慢跑去了mpentry_start
。
mpentry_start
1 | /* See COPYRIGHT for copyright information. */ |
再看看mp_main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// Setup code for APs
void
mp_main(void)
{
// We are in high EIP now, safe to switch to kern_pgdir
lcr3(PADDR(kern_pgdir)); //加载内核页
cprintf("SMP: CPU %d starting\n", cpunum());
lapic_init();//这三个初始化一些东西,应该看的出初始化了啥。
env_init_percpu();
trap_init_percpu();
xchg(&thiscpu->cpu_status, CPU_STARTED); // tell boot_aps() we're up
// Now that we have finished some basic setup, call sched_yield()
// to start running processes on this CPU. But make sure that
// only one CPU can enter the scheduler at a time!
//
// Your code here:
lock_kernel();//这个是内核锁,后面会讲的。
// Remove this after you finish Exercise 6
//for (;;);
sched_yield();
}
我们练习2
让我们再开一些内存给他启动用。这个地方我在思考,运行完之后难道不用把这块内存重新加入内存空闲列表吗??在我们page_init
后面继续添加几行就可以了。1
2
3
4
5
6
7// LAB 4:
// Change your code to mark the physical page at MPENTRY_PADDR
// as in use
// 把MPENTRY_PADDR这块地址也在空闲列表里面删除。
uint32_t range_mpentry = PGNUM(MPENTRY_PADDR);
pages[range_mpentry+1].pp_link=pages[range_mpentry].pp_link;
pages[range_mpentry].pp_link=NULL;
Question
- boot.S中,由于尚没有启用分页机制,所以我们能够指定程序开始执行的地方以及程序加载的地址;但是,在mpentry.S的时候,由于主CPU已经处于保护模式下了,因此是不能直接指定物理地址的,给定线性地址,映射到相应的物理地址是允许的。
Per-CPU State and Initialization
在多处理器CPU中,知道自己是哪个CPU十分重要。前面我们已经分析过怎么获取所有CPU的信息 (假装我们知道过程)。kern/cpu.h
里面定义了各种我们想要的信息。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
40
41
42
43
44
45
46
47
48
49
50/*
Kernel-private definitions for multiprocessor support
多处理器支持的私有内核定义
应该是 定义了 多处理器的一些操作
*/
// Maximum number of CPUs
// Values of status in struct Cpu
enum { //这个是CPU状态
CPU_UNUSED = 0,
CPU_STARTED,
CPU_HALTED,
};
// Per-CPU state
struct CpuInfo { //CPU信息
uint8_t cpu_id; // Local APIC ID; index into cpus[] below 第几个CPU
volatile unsigned cpu_status; // The status of the CPU 状态
struct Env *cpu_env; // The currently-running environment. 当前运行的环境
struct Taskstate cpu_ts; // Used by x86 to find stack for interrupt cpu中断栈
};
// Initialized in mpconfig.c
extern struct CpuInfo cpus[NCPU]; //这几个就是收集到的信息...
extern int ncpu; // Total number of CPUs in the system 数量
extern struct CpuInfo *bootcpu; // The boot-strap processor (BSP) BSP的信息
extern physaddr_t lapicaddr; // Physical MMIO address of the local APIC 物理地址
// Per-CPU kernel stacks
extern unsigned char percpu_kstacks[NCPU][KSTKSIZE]; ///每个CPU的内核栈
int cpunum(void); //获取自己这个CPU的id
void mp_init(void); //收集所有CPU信息
void lapic_init(void);//中断初始化
void lapic_startap(uint8_t apicid, uint32_t addr);//CPU启动
void lapic_eoi(void);//Acknowledge interrupt.
void lapic_ipi(int vector);//不知道是啥
每个CPU独有的属性:
Per-CPU kernel stack
,因为不同的CPU可能同时陷入到内核,因此每个CPU需要有不同的内核栈防止彼此之间的干扰。数组percpu_kstacks[NCPU][KSTKSIZE]
给NCPU个CPU保留了内核栈的空间。在lab2中,将物理地址bootstack映射到BSP的内核栈的虚拟地址KSTACKTOP-KSTKSIZE
。相似的,在本次实验中,你将映射每个CPU的内核栈到这个区域,并且每个栈之间相隔一个guard pages
作为缓冲。CPU0的栈将从KSTACKTOP
向下增长,CPU 1的栈将在CPU 0的栈增长方向的底部之后的KSTKGAP
字节开始。Per-CPU TSS and TSS descriptor
,每个CPU的都需要任务状态段用以区分不同的CPU内核栈的位置。CPU i的TSS
在cpus[i].cpu_ts
中存储,相应的TSS
描述符在GDT
表项gdt[(GD_TSS0 >> 3) + i]
中。定义在kern/trap
全局变量ts
将不会再使用。Per-CPU current environment pointer
,由于每个CPU可以同时运行不同的用户环境,我们定义符号curenv
表示cpus[cpunum()].cpu_env
(或者是thiscpu->cpu_env
),指向正在当前CPU上运行的用户环境。Per-CPU system registers
,包括系统寄存器在内的所有寄存器对每个CPU来说都是私有的。因此,初始化这些寄存器的指令,如lcr3(), ltr(), lgdt(), lidt()
等等必须在每个CPU上执行一次。函数env_init_percpu()
和trap_init_percpu()
就是为了实现这个功能。
练习3
让我实现内存每个CPU的栈分配,在kern/pmap.c
中的mem_init_mp()
。
mem_init_mp
1 | // Modify mappings in kern_pgdir to support SMP |
练习 4
让我们实现每个CPU的中断初始化,在 kern/trap.c
中的trap_init_percpu()
。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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55// Initialize and load the per-CPU TSS and IDT 初始化每个CPU的TSS 和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.
//已经有了一个TSS描述关于CPU 0,但是我们需要初始化多个CPU的
// Hints:
// - The macro "thiscpu" always refers to the current CPU's
// struct CpuInfo; 用thiscpu 指向当前CPU 的CPUinfo
// - The ID of the current CPU is given by cpunum() or
// thiscpu->cpu_id; 获取ID
// - Use "thiscpu->cpu_ts" as the TSS for the current CPU,
// rather than the global "ts" variable; 获取ts
// - Use gdt[(GD_TSS0 >> 3) + i] for CPU i's TSS descriptor;获取 TSS 描述
// - 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!) 初始化 cpu_ts.ts_iomb
//
// ltr sets a 'busy' flag in the TSS selector, so if you
// accidentally load the same TSS on more than one CPU, you'll 每个CPU的TSS 不一样
// get a triple fault. If you set up an individual CPU's TSS 如果相同的TSS 就会报错
// wrong, you may not get a fault until you try to return from
// user space on that CPU.
//
// LAB 4: Your code here
int i=thiscpu->cpu_id;//直接把 ts 改成thiscpu->cpu_ts
thiscpu->cpu_ts.ts_esp0=KSTACKTOP-i*(KSTKSIZE+KSTKGAP);//地址要变
thiscpu->cpu_ts.ts_ss0=GD_KD;
thiscpu->cpu_ts.ts_iomb = sizeof(struct Taskstate);
//初始化gdt 根据前面的来就行了
gdt[(GD_TSS0 >> 3) + i] = SEG16(STS_T32A, (uint32_t) (&(thiscpu->cpu_ts)),
sizeof(struct Taskstate) - 1, 0);
gdt[(GD_TSS0 >> 3) + i].sd_s = 0;
// // Setup a TSS so that we get the right stack
// // when we trap to the kernel.
// ts.ts_esp0 = KSTACKTOP;
// ts.ts_ss0 = GD_KD;
// ts.ts_iomb = sizeof(struct Taskstate);
// // Initialize the TSS slot of the gdt.
// gdt[GD_TSS0 >> 3] = SEG16(STS_T32A, (uint32_t) (&ts),
// sizeof(struct Taskstate) - 1, 0);
// gdt[GD_TSS0 >> 3].sd_s = 0;
// Load the TSS selector (like other segment selectors, the
// bottom three bits are special; we leave them 0)
ltr(GD_TSS0+8*i); //每个占3位 也就是 1<<3=8
// Load the IDT
lidt(&idt_pd);
}
运行make qemu CPUS=4
就会出现官网上的那些东西。
Locking
大内核锁,简单来讲,就是当一个CPU进入内核的时候,内核锁住,因为多个CPU同是在内核里面运行可能出错。可以自行百度一下。
在kern/spinlock.h
定义了那些锁。我们去看看。
kern/spinlock.c
1 | // Mutual exclusion spin locks. |
里面用的上的函数,也就两个spin_lock
和spin_unlock
,他们在spinlock.h
里面用lock_kernel
和unlock_kernel
调用。
在代码中总共有4处使用了大内核锁:
- 在
i386_init()
函数中,BSP先获得大内核锁然后再启动其余的CPU - 在
mp_main()
函数中,在初始化AP后获得大内核锁,然后调用sched_yield()
开始在这个AP上运行用户环境。 - 在
trap()
函数中,从用户态陷入到内核态必须获得大内核锁,通过检查tf_cs
的低位确定这个陷入发生在用户态还是在内核态 - 在
env_run()
函数中,在切换到用户态之前释放大内核锁,不要太早也不要太晚,否则就会体验一把竞争或者死锁的情况。
练习5
就是让我们在这几个地方调用。
第一个 i386_init
里面1
2
3
4
5// Acquire the big kernel lock before waking up APs
// Your code here:
lock_kernel();
// Starting non-boot CPUs 在这个启动之前调用lock_kernel();
boot_aps();
第二个 mp_main
里面1
2
3
4
5
6
7
8
9
10
// Now that we have finished some basic setup, call sched_yield()
// to start running processes on this CPU. But make sure that
// only one CPU can enter the scheduler at a time!
//
// Your code here:
lock_kernel();//锁住内核
// Remove this after you finish Exercise 6
//for (;;); 这个可以注释掉了,虽然说是练习 6,等会注释也是一样的 后面是调度程序
sched_yield();
第三个trap
1
2
3
4
5
6if ((tf->tf_cs & 3) == 3) {
// Trapped from user mode. 如果是从用户模式过来就锁住内核。
// Acquire the big kernel lock before doing any
// serious kernel work.
// LAB 4: Your code here.
lock_kernel();
第4个env_run()
这个函数跑用户态去了,所以要释放内核。1
2unlock_kernel(); //在转移之前释放内核
env_pop_tf(&curenv->env_tf);
其实还用很多锁住内核,和释放内核,但是我们实验并没有让我们实现。
Question 2
没解决告辞。
Round-Robin Scheduling
实现轮转调度。
kern/sched.c
中的sched_yield()
函数负责选取一个新用户环境运行。从刚刚运行的用户环境开始以循环的方式依次搜索envs[]
数组(如果之前没有运行过的用户环境,就从数组的第一个元素开始),选择发现的第一个状态为ENV_RUNNABLE
的用户环境,然后调用env_run()
跳转到选中的用户环境上运行。sched_yield()
不能同时在两个CPU上运行相同的用户环境。通过判断用户环境的状态就可以确定该环境是否正在运行- 我们已经实现了一个新的系统调用
sys_yield()
,用户环境可以调用以执行内核态的sched_yield()
实现以自动放弃CPU的控制权。
练习6
让我们实现这个调度程序。
sched_yield
1 |
|
实现了sched_yield
我们还需要在系统调用里面使用他,不然就不会从一个环境里面出来。
在syscall.c
里面定义了一个调用他的syscall
。然后我们需要使用他。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// Deschedule current environment and pick a different one to run.
static void
sys_yield(void)
{
sched_yield();
}
//在syscall()里面加入 SYS_yield
switch (syscallno) {
case (SYS_cputs):
sys_cputs((const char *)a1, a2);
return 0;
case (SYS_cgetc):
return sys_cgetc();
case (SYS_getenvid):
return sys_getenvid();
case (SYS_env_destroy):
return sys_env_destroy(a1);
case (SYS_yield)://多加入这一行
sys_yield();
return 0;
再在mp_main
最后调用一下注释掉无线循环。1
2
3// Remove this after you finish Exercise 6
//for (;;);
sched_yield();
然后我们需要验证一下,要在init
里面添加测试样例。1
2
3
4
5
6
7
8
9
10
11
12
// Don't touch -- used by grading script!
ENV_CREATE(TEST, ENV_TYPE_USER);
// Touch all you want.
// ENV_CREATE(user_primes, ENV_TYPE_USER);//把这个歌注释掉,添加下面 3个进程
ENV_CREATE(user_yield, ENV_TYPE_USER);
ENV_CREATE(user_yield, ENV_TYPE_USER);
ENV_CREATE(user_yield, ENV_TYPE_USER);
然后运行make qemu CPUS=2
可以看到和官网上说的一样的结果。
为什么会出现这种结果可以查看user/yield.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// yield the processor to other environments
void
umain(int argc, char **argv)
{
int i;
cprintf("Hello, I am environment %08x.\n", thisenv->env_id);
for (i = 0; i < 5; i++) {
sys_yield();
cprintf("Back in environment %08x, iteration %d.\n",
thisenv->env_id, i);
}
cprintf("All done in environment %08x.\n", thisenv->env_id);
}
Question 3
这个问题是,为什么lrc3
切换了页表但是,对于进程的e
指针还是不用变,因为[UENVS, UENVS+PTSIZE)
的映射物理地址都是一样的。
Question 4
为什么要保存,寄存器的状态。特么还要问么。告辞。因为不保存下来就无法正确地恢复到原来的环境。
System Calls for Environment Creation
现在我们的系统已经能够环境运行了但是还是不能用户创建进程,在unix
中我们用的fork
函数创建进程,所以我们现在要实现一个简单fork
函数。
为了实现这个函数,我们需要下面这些系统调用。
sys_exofork
:这个系统调用将创建一个新的空白用户环境,没有映射的用户空间且无法运行。在调用函数时新用户环境的寄存器状态与父进程相同。在父用户环境中,会返回子用户环境的envid_t
(如果用户环境分配失败,返回一个负值的错误码)。而子用户环境中,会返回0。(由于子用户环境开始标记为不可运行,sys_exofork
实际上是不会返回到子用户环境直到父用户环境标记子用户环境可以运行…)sys_env_set_status
:这个系统调用将特定用户环境的状态设置为ENV_RUNNABLE
或者ENV_NOT_RUNNABLE
。一旦一个新的用户环境的地址空间和所有寄存器都完全初始化,这个系统调用用来标记这个用户环境准备运行。sys_page_alloc
:分配一个页的物理内存,并将其映射到给定用户环境地址空间的给定虚拟地址。sys_page_map
:从一个用户环境拷贝一个页的映射到另外一个用户环境,这样就完成了内存共享,使新旧的映射都是指同一页的物理内存。
sys_page_unmap
:取消给定用户环境给定虚拟地址的映射。
以上所有的系统调用都接收用户环境ID,JOS内核支持将0作为当前运行的用户环境的ID的惯例,这个惯例通过kern/env.c
中的envid2env()
实现。
我们需要实现fork
来通过 user/dumbfork.c
。我们先去看看这个程序做了啥。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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79// Ping-pong a counter between two processes.
// Only need to start one of these -- splits into two, crudely.
envid_t dumbfork(void);
void
umain(int argc, char **argv)
{
envid_t who;
int i;
// fork a child process
who = dumbfork();//可以简单认为这就是个fork 函数
// print a message and yield to the other a few times
for (i = 0; i < (who ? 10 : 20); i++) {
cprintf("%d: I am the %s!\n", i, who ? "parent" : "child");
sys_yield();//输出完后就调度
}
}
void
duppage(envid_t dstenv, void *addr)
{
int r;
// This is NOT what you should do in your fork.
if ((r = sys_page_alloc(dstenv, addr, PTE_P|PTE_U|PTE_W)) < 0)//开辟了一个空间
panic("sys_page_alloc: %e", r);
if ((r = sys_page_map(dstenv, addr, 0, UTEMP, PTE_P|PTE_U|PTE_W)) < 0)//映射了空间
panic("sys_page_map: %e", r);
memmove(UTEMP, addr, PGSIZE);//复制一份
if ((r = sys_page_unmap(0, UTEMP)) < 0)//取消映射。
panic("sys_page_unmap: %e", r);
}
envid_t
dumbfork(void)
{
envid_t envid;
uint8_t *addr;
int r;
extern unsigned char end[];
// Allocate a new child environment.
// The kernel will initialize it with a copy of our register state,
// so that the child will appear to have called sys_exofork() too -
// except that in the child, this "fake" call to sys_exofork()
// will return 0 instead of the envid of the child.
envid = sys_exofork();
if (envid < 0)
panic("sys_exofork: %e", envid);
if (envid == 0) {
// We're the child.
// The copied value of the global variable 'thisenv'
// is no longer valid (it refers to the parent!).
// Fix it and return 0.
thisenv = &envs[ENVX(sys_getenvid())];//如果是儿子就把新环境重新指向一下
return 0;
}
// We're the parent.
// Eagerly copy our entire address space into the child.
// This is NOT what you should do in your fork implementation.
for (addr = (uint8_t*) UTEXT; addr < end; addr += PGSIZE)//如果是父亲我们需要拷贝一份地址
duppage(envid, addr);//这个韩式自己看一下
// Also copy the stack we are currently running on.
duppage(envid, ROUNDDOWN(&addr, PGSIZE));//复制栈
// Start the child environment running
if ((r = sys_env_set_status(envid, ENV_RUNNABLE)) < 0)//唤醒儿子
panic("sys_env_set_status: %e", r);
return envid;
}
简单来讲解释写了一个简单的fork
程序通过系统调用把内存复制了一份(这个时候还没有写时复制,所以是直接copy内存的),然后输出了一些值。
在我们写系统调用fork
之前需要看看envid2env
。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
40
41
42
43
44
45//
// Converts an envid to an env pointer. 把id 转换成env
// If checkperm is set, the specified environment must be either the
// current environment or an immediate child of the current environment.
//需不需要判断是当前进程或者子进程
// RETURNS
// 0 on success, -E_BAD_ENV on error. //0成功其他出错
// On success, sets *env_store to the environment. //成功设置环境
// On error, sets *env_store to NULL.//不成功保存NULL
//
int
envid2env(envid_t envid, struct Env **env_store, bool checkperm)
{
struct Env *e;
//如果id是 0直接返回当前环境
// If envid is zero, return the current environment.
if (envid == 0) {
*env_store = curenv;
return 0;
}
// Look up the Env structure via the index part of the envid,
// then check the env_id field in that struct Env
// to ensure that the envid is not stale
// (i.e., does not refer to a _previous_ environment
// that used the same slot in the envs[] array).
e = &envs[ENVX(envid)];
if (e->env_status == ENV_FREE || e->env_id != envid) {//如果进程已经释放,就GG
*env_store = 0;
return -E_BAD_ENV;
}
// Check that the calling environment has legitimate permission
// to manipulate the specified environment.
// If checkperm is set, the specified environment
// must be either the current environment
// or an immediate child of the current environment.//判断是不是自己或者子进程
if (checkperm && e != curenv && e->env_parent_id != curenv->env_id) {
*env_store = 0;
return -E_BAD_ENV;
}
*env_store = e;
return 0;
}
所以说说上面就是判断一下进程是不是可用的。如果chekperm
是1
还需要检查是不是当前进程是不是当前进程或子进程。练习7
实现前面说额那几个函数了。
第一个sys_exofork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// Allocate a new environment. 分配一个新的进程,你可以理解成PCB
// Returns envid of new environment, or < 0 on error. Errors are:
// -E_NO_FREE_ENV if no free environment is available. 没有进程可以用了返回
// -E_NO_MEM on memory exhaustion. 没有内存了返回
static envid_t
sys_exofork(void)
{
// Create the new environment with env_alloc(), from kern/env.c.用env_alloc分配进程
// It should be left as env_alloc created it, except that 设置成ENV_NOT_RUNNABLE
// 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. 需要把返回值设置成0
// LAB 4: Your code here.
struct Env*child=NULL;
int r=env_alloc(&child,curenv->env_id);
if(r!=0)return r;
child->env_tf=curenv->env_tf; //复制tf,这个tf当前运行的位置应该是fork 之后的第一条语句
child->env_status=ENV_NOT_RUNNABLE; //设置环境
//cprintf("status:%d\n",child->env_status);
child->env_tf.tf_regs.reg_eax = 0;//返回值变成0
return child->env_id; //父亲返回的是儿子的id
//panic("sys_exofork not implemented");
}
下面就是sys_env_set_status
更改进程状态。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// Set envid's env_status to status, which must be ENV_RUNNABLE
// or ENV_NOT_RUNNABLE. 更改的状态必须是 ENV_RUNNABLE 和ENV_NOT_RUNNABLE
//
// Returns 0 on success, < 0 on error. Errors are: 失败返回<0
// -E_BAD_ENV if environment envid doesn't currently exist,
// or the caller doesn't have permission to change envid. //如果环境不存在或者进程错误
// -E_INVAL if status is not a valid status for an environment. 如果值错了
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. 用envid2env来检查进程
// 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.
struct Env * env=NULL;
int r=envid2env(envid,&env,1);//检查进程id是不是对的
if(r<0)return -E_BAD_ENV;
else {
if(status!=ENV_NOT_RUNNABLE&&status!=ENV_RUNNABLE)return -E_INVAL;//检查环境值是不是对的
env->env_status=status;
}
return 0;
//panic("sys_env_set_status not implemented");
}
然后就是关于内存的sys_page_alloc
,sys_page_map
,sys_page_unmap
,都差不多。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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113// Allocate a page of memory and map it at 'va' with permission
// 'perm' in the address space of 'envid'. 分配一个页的内存映射 envid
// The page's contents are set to 0. 页面内容设置为 0,也就是初始化为0
// If a page is already mapped at 'va', that page is unmapped as a
// side effect. 如果va是已经映射的就需要,取消映射
//
// perm -- PTE_U | PTE_P must be set, PTE_AVAIL | PTE_W may or may not be set, PTE_U | PTE_P权限必须设置 PTE_AVAIL | PTE_W 可以不设置
// but no other bits may be set. See PTE_SYSCALL in inc/mmu.h.
// 其他权限 PTE_SYSCALL 也许可以被设置,意味着超过这个权限都是错的。
// Return 0 on success, < 0 on error. Errors are: 失败返回 负数
// -E_BAD_ENV if environment envid doesn't currently exist,
// or the caller doesn't have permission to change envid.//环境id错误
// -E_INVAL if va >= UTOP, or va is not page-aligned.//地址不在用户状态或者不是页对齐
// -E_INVAL if perm is inappropriate (see above). 权限错误
// -E_NO_MEM if there's no memory to allocate the new page,//没有内存了
// or to allocate any necessary page tables.
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. 可以使用page_alloc和page_insert
// 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 * env;
if(envid2env(envid,&env,1)<0)return -E_BAD_ENV;//判断进程
if((uintptr_t)va>=UTOP||PGOFF(va))return -E_INVAL;//判断地址
int flag=PTE_U | PTE_P;
if((perm & ~(PTE_SYSCALL))!=0||(perm&flag)!=flag)return -E_INVAL;//判断权限
struct PageInfo* pi=page_alloc(1);//分配一个页
if(pi==NULL)return -E_NO_MEM;
if(page_insert(env->env_pgdir,pi,va,perm)<0){//映射上去
page_free(pi);
return -E_NO_MEM;
}
return 0;
//panic("sys_page_alloc not implemented");
}
// Map the page of memory at 'srcva' in srcenvid's address space
// at 'dstva' in dstenvid's address space with permission 'perm'.
// Perm has the same restrictions as in sys_page_alloc, except
// that it also must not grant write access to a read-only
// page.
//这个是把 源 虚拟地址映射到 目的 虚拟地址
// Return 0 on success, < 0 on error. Errors are://一堆错误提示
// -E_BAD_ENV if srcenvid and/or dstenvid doesn't currently exist,
// or the caller doesn't have permission to change one of them.
// -E_INVAL if srcva >= UTOP or srcva is not page-aligned,
// or dstva >= UTOP or dstva is not page-aligned.
// -E_INVAL is srcva is not mapped in srcenvid's address space.
// -E_INVAL if perm is inappropriate (see sys_page_alloc).
// -E_INVAL if (perm & PTE_W), but srcva is read-only in srcenvid's
// address space.
// -E_NO_MEM if there's no memory to allocate any necessary page tables.
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.
int r=0;
struct Env * srccur=NULL,*dstcur=NULL;
r=envid2env(srcenvid,&srccur,1);
if(r<0)return -E_BAD_ENV;
r=envid2env(dstenvid,&dstcur,1);//判断两个进程
if(r<0)return -E_BAD_ENV;
if((uintptr_t)srcva >= UTOP||(uintptr_t)dstva >= UTOP||PGOFF(srcva)|| PGOFF(dstva))return -E_INVAL;//判断页地址和目的地址
pte_t * store=NULL;
struct PageInfo* pg=NULL;
if((pg=page_lookup(srccur->env_pgdir,srcva,&store))==NULL)return -E_INVAL;//查看一个页
int flag=PTE_U | PTE_P;
if((perm & ~(PTE_SYSCALL))!=0||(perm&flag)!=flag)return -E_INVAL;
if((perm&PTE_W)&&!(*store&PTE_W))return E_INVAL;//判断权限
if (page_insert(dstcur->env_pgdir, pg, dstva, perm) < 0) //插入到一个页
return -E_NO_MEM;
return 0;
//panic("sys_page_map not implemented");
}
// Unmap the page of memory at 'va' in the address space of 'envid'.
// If no page is mapped, the function silently succeeds.
//取消一个进程 对va 的映射。
// Return 0 on success, < 0 on error. Errors are:
// -E_BAD_ENV if environment envid doesn't currently exist,
// or the caller doesn't have permission to change envid.
// -E_INVAL if va >= UTOP, or va is not page-aligned.
static int
sys_page_unmap(envid_t envid, void *va)
{
// Hint: This function is a wrapper around page_remove().
// LAB 4: Your code here.
struct Env *env;
int r=envid2env(envid,&env,1);
if(r<0)return -E_BAD_ENV;
if((uintptr_t)va>=UTOP||PGOFF(va))return -E_INVAL;
page_remove(env->env_pgdir,va);
return 0;
//panic("sys_page_unmap not implemented");
}
然后就可以运行了。
最后不要忘记把他填到syscall
里面。1
2
3
4
5
6
7
8
9
10case SYS_exofork:
return sys_exofork();
case SYS_env_set_status:
return sys_env_set_status((envid_t)a1, (int)a2);
case SYS_page_alloc:
return sys_page_alloc((envid_t)a1, (void *)a2, (int)a3);
case SYS_page_map:
return sys_page_map((envid_t)a1, (void *)a2, (envid_t)a3, (void *)a4, (int)a5);
case SYS_page_unmap:
return sys_page_unmap((envid_t)a1, (void *)a2);
然后就完成了PART A
了。
Part B: Copy-on-Write Fork
写时复制,对于这个机制应该都很清楚。大部分程序fork
之后就调用了exec
所以,我门,并没有复制内存,也就是少了dumbfork
里面的memmove(UTEMP, addr, PGSIZE);
。但是这样做就有了个缺陷,如果没有调用exec
,子进程又访问了就要进行缺页中断。所以这次我我们的任务就是实现这些东西。
User-level page fault handling
一个用户级写时拷贝的fork函数需要知道哪些page fault是在写保护页时触发的,写时复制只是用户级缺页中断处理的一种。
通常建立地址空间以便page fault提示何时需要执行某些操作。例如大多数Unix内核初始只给新进程的栈映射一个页,以后栈增长会导致page fault从而映射新的页。一个典型的Unix内核必须记录在进程地址空间的不同区域发生page fault时,应该执行什么操作。例如栈上缺页,会实际分配和映射新的物理内存。BSS区域缺页会分配新的物理页,填充0,然后映射。这种设计在定义他们的内存区域的时候具有极大的灵活度。
Setting the Page Fault Handler
为了处理自己的缺页中断,用户环境需要在JOS内核中注册缺页中断处理程序的入口。用户环境通过sys_env_set_pgfault_upcall
系统调用注册它的缺页中断入口。我们在Env结构体中增加了一个新成员env_pgfault_upcall
来记录这一信息。练习8
就是让你实现缺页中断的入口,就是你用写时复制,如果修改了该怎么处理,调用哪个程序去处理。我们需要去实现这个sys_env_set_pgfault_upcall
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// Set the page fault upcall for 'envid' by modifying the corresponding struct
// Env's 'env_pgfault_upcall' field. When 'envid' causes a page fault, the
// kernel will push a fault record onto the exception stack, then branch to
// 'func'.
//参数传进去一个函数指针,直接把处理缺页中断的变量指向就可以了
// Returns 0 on success, < 0 on error. Errors are:
// -E_BAD_ENV if environment envid doesn't currently exist,
// or the caller doesn't have permission to change envid.
static int
sys_env_set_pgfault_upcall(envid_t envid, void *func)
{
// LAB 4: Your code here.
struct Env * env;
if(envid2env(envid,&env,1)<0)return -E_BAD_ENV;//先判断进程可不可以用
env->env_pgfault_upcall=func;//意思就是处理中断的时候用func 这个函数。
return 0;
//panic("sys_env_set_pgfault_upcall not implemented");
}
千万别忘记把这个添进syscall
,我这个地方忘记添加了,找了半天不知道为啥。1
2case SYS_env_set_pgfault_upcall:
return sys_env_set_pgfault_upcall(a1,(void *)a2);
Normal and Exception Stacks in User Environments
在正常运行期间,用户进程运行在用户栈上,开始运行时栈顶寄存器ESP
指向USTACKTOP
,压入堆栈的数据位于[USTACKTOP-PGSIZE ~ USTACKTOP-1]
之间的页。当一个页错误出现在用户模式下,内核重启用户环境让其在用户异常栈上运行指定的用户级缺页处理程序。我们将使JOS代替用户环境实现自动的“栈切换”,就如同x86处理器代替JOS内核实现从用户模式到内核模式的栈切换。
JOS的用户异常栈的大小为一个页,初始栈顶定义在UXSTACKTOP
。因此有效的用户异常栈的区间是[UXSTACKTOP-PGSIZE ~ UXSTACKTOP-1]
。运行在异常栈上的用户级的页错误处理程序可以使用JOS的常规的系统调用,来映射新的页或者调整映射,来修复导致页错误的问题。然后用户级别页错误处理程序通过一个汇编语言stub返回到原始栈的错误代码处。
每一个想要支持用户级别页错误处理的用户环境都需要为自己的异常栈分配内存,这就用到了在part A
中引入的sys_page_alloc()
系统调用函数。
这个时候我们就需要一个新的栈,叫做用户异常栈。
Invoking the User Page Fault Handler
我们现在需要修改kern/trap.c
里面的用户模式的缺页错误,因为现在我们有了用户的缺页处理函数。现在我们如果设置了,缺页处理函数,就调用缺页处理函数,没有我们就销毁这个进程。
1 | void |
User-mode Page Fault Entrypoint
接下来,就需要实现汇编例程负责调用C的缺页异常处理程序并恢复执行原来出错的指令。这个汇编例程(lib/pfentry.S
中的_pgfault_upcall
)就是需要使用sys_env_set_pgfault_upcall()
系统调用注册到内核的处理程序。
这个练习我没看懂。所以我就直接 贴别人的代码了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Struct PushRegs size = 32
addl $8, %esp // esp+8 -> PushRegs over utf_fault_va utf_err
movl 0x20(%esp), %eax // eax = (esp+0x20 -> utf_eip )
subl $4, 0x28(%esp) // for trap time eip 保留32bit, esp+48 = utf_esp
movl 0x28(%esp), %edx // %edx = utf_esp-4
movl %eax, (%edx) // %eax = eip ----> esp-4 以至于ret可以直接读取其继续执行的地址
popal // after popal esp->utf_eip
addl $4, %esp // esp+4 -> utf_eflags
popfl
popl %esp
ret // 这里十分巧妙, ret会读取esp指向的第一个内容, 也就是我们第一步写入的eip
练习11
就是让你实现lib/pgfault.c.
里面的set_pgfault_handler。
1 | // |
Implementing Copy-on-Write Fork
最后就是实现写时复制了。
前面我们有一个测试程序,user/dumbfork
,这个里面已经有了模板,我们现在要做的就是实现一个差不多的fork
。
他的基本流程是:
- 父进程将
pgfault()
函数作为C语言实现的页错误处理,会用到上面的实现的set_pgfault_handler()
函数进行设置。父进程调用sys_exofork()
创建一个子进程环境。 - 在
UTOP
之下的在地址空间里的每一个可写或copy-on-write
的页,父进程就会调用duppage
,它会将copy-on-write
页映射到子进程的地址空间,然后重新映射copy-on-write
页到自己的地址空间。[注意这里的顺序十分重要!先将子进程的页标记为COW
,然后将父进程的页标记为COW
。知道为什么吗?你可以尝试思考将该顺序弄反会是造成怎样的麻烦]。duppage
将COW
的页的PTEs
设置为不能写的,然后在PTE
的avail
域设置PTE_COW
来区别copy-on-write pages
及真正的只读页 - 异常栈并不是如上重新映射,在子进程中需要为异常栈分配一个新的页。由于缺页异常处理程序将执行实际的拷贝,而且缺页异常处理程序在异常栈上运行,异常栈不应该被设置为
cow
。fork()
同样要解决在内存中的页,但页既不可写也不是copy-on-write
。 - 父进程为子进程设置用户页错误入口。
- 子进程现在可以运行,然后父进程将其标记为可运行。
每次这两进程中的一个向一个尚未写过的copy-on-write
页写时,就会产生一个页错误。下面是用户页错误处理的控制流:
- 内核传播页错误到
_pgfault_upcall
,调用fork()
的pgfault()
处理流程。 pgfault()
检查错误代码中的FEC_WR
(即是写导致的),以及页对应的PTE
标记为PTE_COW
。没有的话,panic
。pgfault()
分配一个映射在一个临时位置的新的页,然后将错误页中的内容复制进去。然后页错误处理程序映射新的页到引起page fault
的虚拟地址,并设置PTE
具有读写权限。
用户级的lib/fork.c
必须访问用户环境的页表完成以上的几个操作(例如将一个页对应的PTE
标记为PTE_COW
)。内核映射用户环境的页表到虚拟地址UVPT
的用意就在于此。它使用了一种聪明的手段让用户代码很方便的检索PTE
。lib/entry.S
设置uvpt
和uvpd
使得lib/fork.c
中的用户代码能够轻松地检索页表信息。练习12
就是让我们实现fork.c
里面的fork
, duppage
和 pgfault
。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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141// implement fork from user space
// PTE_COW marks copy-on-write page table entries.
// It is one of the bits explicitly allocated to user processes (PTE_AVAIL).
//
// Custom page fault handler - if faulting page is copy-on-write,
// map in our own private writable copy.
// 用户处理缺页
static void
pgfault(struct UTrapframe *utf)
{
void *addr = (void *) utf->utf_fault_va;
uint32_t err = utf->utf_err;
int r;
// Check that the faulting access was (1) a write, and (2) to a
// copy-on-write page. If not, panic.
// Hint: 检查是不是因为因为写入导致的错误,不是就paic
// Use the read-only page table mappings at uvpt
// (see <inc/memlayout.h>). uvpt 和uvpd 在memlayout 这个里面有定义,很久之前我们就看过了。 一个页目录一个是页表的。
// LAB 4: Your code here.
if (!(
(err & FEC_WR) && (uvpd[PDX(addr)] & PTE_P) &&
(uvpt[PGNUM(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_COW)
)) panic("Neither the fault is a write nor copy-on-write page.\n");//如果不是因为这个原因 就panic
// 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.
if((r = sys_page_alloc(0, PFTEMP, PTE_U | PTE_P | PTE_W)) < 0){
panic("sys_page_alloc: %e\n", r);//分配了一个页
}
addr = ROUNDDOWN(addr, PGSIZE);//页对齐
memcpy((void *)PFTEMP, addr, PGSIZE);//把这个写时复制的页内容复制一遍
if ((r = sys_page_map(0, (void *)PFTEMP, 0, addr, PTE_P | PTE_U | PTE_W)) < 0)
panic("sys_page_map: %e\n", r);//把当前映射的 地址 指向PFTEMP 新分配的页
if ((r = sys_page_unmap(0, (void *)PFTEMP)) < 0) //取消PFTEMP 的映射,这样就把虚拟地址指向了一个新的页。
panic("sys_page_unmap: %e\n", r);
//panic("pgfault not implemented");
}
//
// Map our virtual page pn (address pn*PGSIZE) into the target envid
// at the same virtual address. If the page is writable or copy-on-write,
// the new mapping must be created copy-on-write, and then our mapping must be
// marked copy-on-write as well. (Exercise: Why do we need to mark ours
// copy-on-write again if it was already copy-on-write at the beginning of
// this function?)
//把 我们虚拟页 pn*PGSIZE映射到 相同的虚拟地址,如果原本就是写时复制那么新的也要标记成 写时复制
// Returns: 0 on success, < 0 on error.
// It is also OK to panic on error.
//
static int
duppage(envid_t envid, unsigned pn)
{
int r;
// LAB 4: Your code here.
void* vaddr=(void*)(pn*PGSIZE);
if((uvpt[pn] & PTE_W) || (uvpt[pn] & PTE_COW)){
if ((r = sys_page_map(0, vaddr, envid, vaddr, PTE_P | PTE_U | PTE_COW)) < 0)
return r;//映射当前页为写时符合
if ((r = sys_page_map(0, vaddr, 0, vaddr, PTE_P | PTE_U | PTE_COW)) < 0)
return r;//把自己当前页页标记成写时复制。
}
else if((r = sys_page_map(0, vaddr, envid, vaddr, PTE_P | PTE_U)) < 0) {
return r;//如果当前页已经是写时复制 就不需要更改了
}
//panic("duppage not implemented");
return 0;
}
//
// User-level fork with copy-on-write. 写时复制
// Set up our page fault handler appropriately.设置缺页处理
// Create a child. 创建一个儿子
// Copy our address space and page fault handler setup to the child. 复制空间和设置缺页处理
// Then mark the child as runnable and return. 标记儿子为 runable
//
// Returns: child's envid to the parent, 0 to the child, < 0 on error.
// It is also OK to panic on error. 父亲返回 儿子id 儿子返回 0 返回 <0 出错
//
// Hint:
// Use uvpd, uvpt, and duppage. 使用 uvpd, uvpt, 和 duppage
// Remember to fix "thisenv" in the child process.
// Neither user exception stack should ever be marked copy-on-write,
// so you must allocate a new page for the child's user exception stack.
// 不用把异常栈标记为写时复制 所以必须分配新的一页给儿子
envid_t
fork(void)
{
// LAB 4: Your code here.
envid_t cenvid;
unsigned pn;
int r;
set_pgfault_handler(pgfault); //设置 缺页处理
if ((cenvid = sys_exofork()) < 0){ //创建了一个进程。
panic("sys_exofork failed");
return cenvid;
}
if(cenvid>0){//如果是 父亲进程
for (pn=PGNUM(UTEXT); pn<PGNUM(USTACKTOP); pn++){ //复制UTEXT 到USTACKTOP的页
if ((uvpd[pn >> 10] & PTE_P) && (uvpt[pn] & PTE_P))
if ((r = duppage(cenvid, pn)) < 0)
return r;
}
if ((r = sys_page_alloc(cenvid, (void *)(UXSTACKTOP-PGSIZE), PTE_U | PTE_P | PTE_W)) < 0) //分配一个新的页
return r;
extern void _pgfault_upcall(void); //缺页处理
if ((r = sys_env_set_pgfault_upcall(cenvid, _pgfault_upcall)) < 0)
return r; //为儿子设置一个缺页处理分支
if ((r = sys_env_set_status(cenvid, ENV_RUNNABLE)) < 0)//设置成可运行
return r;
return cenvid;
}
else {
thisenv = &envs[ENVX(sys_getenvid())];//如果是儿子就直接运行。
return 0;
}
//panic("fork not implemented");
}
// Challenge!
int
sfork(void)
{ //这个挑战的内容,我没看懂要做什么。
panic("sfork not implemented");
return -E_INVAL;
}
Part C: Preemptive Multitasking and Inter-Process communication (IPC)
现在我们要实现抢占式调度和进程间通信。
Clock Interrupts and Preemption
运行user/spin
会死循环。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// Test preemption by forking off a child process that just spins forever.
// Let it run for a couple time slices, then kill it.
void
umain(int argc, char **argv)
{
envid_t env;
cprintf("I am the parent. Forking the child...\n");
if ((env = fork()) == 0) {
cprintf("I am the child. Spinning...\n");
while //(1) 在这个地方死循环了
/* do nothing */;
}
cprintf("I am the parent. Running the child...\n");
sys_yield();
sys_yield();
sys_yield();
sys_yield();
sys_yield();
sys_yield();
sys_yield();
sys_yield();
cprintf("I am the parent. Killing the child...\n");
sys_env_destroy(env);//如果是抢占式 就会在这个地方给毁了子进程。
}
实现抢占式,必须要有硬件的支持。
Interrupt discipline
外部中断(即,设备中断)被称为IRQ
。有16个可能的IRQ,编号从0到15.IRQ编号到IDT表项的映射不是固定的。picirq.c
中的pic_init
将0~15的IRQ编号映射到IDT表项 ,[IRQ_OFFSET ~ IRQ_OFFSET +15]
。
在inc/trap.h
中,IRQ_OFFSET
的值是32.因此IDT表项[32~ 47]
对应0~15
的IRQ编号。例如,时钟中断是IRQ 0,因此IDT[32]
包含内核中的时钟中断处理例程的地址。IRQ_OFFSET
的选择主要是为了设备中断不与处理器异常重叠。
在JOS中,对比xv6 Unix
,我们做了关键的简化。在内核中的时候,外部设备中断基本上是关闭的(像xv6一样,在用户空间打开)。外部设备中断由%eflags
寄存器上的FL_IF
标志位控制。当这个位置位,外部中断使能。这个位可以通过几种途径修改,由于我们的简化,我们仅通过在进入内核时候保存%eflags
寄存器,退出内核时恢复%eflags
寄存器这个过程来修改FL_IF
标志位。
应该确保在用户态FL_IF
标志位是置位的,这样中断才能传递给处理器,并最终被中断代码处理。否则,中断被屏蔽或忽略,直到重新启用中断。Bootloader
最初几条指令就屏蔽了中断,到目前为止,我们从来没有重新启用它们。练习 13
要我像当初实现内部中断一样,实现这几个外部中断。
一样的没啥区别。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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
TRAPHANDLER_NOEC(IRQ0, IRQ_OFFSET)
TRAPHANDLER_NOEC(IRQ1, IRQ_OFFSET+1)
TRAPHANDLER_NOEC(IRQ2, IRQ_OFFSET+2)
TRAPHANDLER_NOEC(IRQ3, IRQ_OFFSET+3)
TRAPHANDLER_NOEC(IRQ4, IRQ_OFFSET+4)
TRAPHANDLER_NOEC(IRQ5, IRQ_OFFSET+5)
TRAPHANDLER_NOEC(IRQ6, IRQ_OFFSET+6)
TRAPHANDLER_NOEC(IRQ7, IRQ_OFFSET+7)
TRAPHANDLER_NOEC(IRQ8, IRQ_OFFSET+8)
TRAPHANDLER_NOEC(IRQ9, IRQ_OFFSET+9)
TRAPHANDLER_NOEC(IRQ10, IRQ_OFFSET+10)
TRAPHANDLER_NOEC(IRQ11, IRQ_OFFSET+11)
TRAPHANDLER_NOEC(IRQ12, IRQ_OFFSET+12)
TRAPHANDLER_NOEC(IRQ13, IRQ_OFFSET+13)
TRAPHANDLER_NOEC(IRQ14, IRQ_OFFSET+14)
TRAPHANDLER_NOEC(IRQ15, IRQ_OFFSET+15)
void IRQ0();
void IRQ1();
void IRQ2();
void IRQ3();
void IRQ4();
void IRQ5();
void IRQ6();
void IRQ7();
void IRQ8();
void IRQ9();
void IRQ10();
void IRQ11();
void IRQ12();
void IRQ13();
void IRQ14();
void IRQ15();
// trap_init
SETGATE(idt[IRQ_OFFSET], 0, GD_KT, IRQ0, 0);
SETGATE(idt[IRQ_OFFSET+1], 0, GD_KT, IRQ1, 0);
SETGATE(idt[IRQ_OFFSET+2], 0, GD_KT, IRQ2, 0);
SETGATE(idt[IRQ_OFFSET+3], 0, GD_KT, IRQ3, 0);
SETGATE(idt[IRQ_OFFSET+4], 0, GD_KT, IRQ4, 0);
SETGATE(idt[IRQ_OFFSET+5], 0, GD_KT, IRQ5, 0);
SETGATE(idt[IRQ_OFFSET+6], 0, GD_KT, IRQ6, 0);
SETGATE(idt[IRQ_OFFSET+7], 0, GD_KT, IRQ7, 0);
SETGATE(idt[IRQ_OFFSET+8], 0, GD_KT, IRQ8, 0);
SETGATE(idt[IRQ_OFFSET+9], 0, GD_KT, IRQ9, 0);
SETGATE(idt[IRQ_OFFSET+10], 0, GD_KT, IRQ10, 0);
SETGATE(idt[IRQ_OFFSET+11], 0, GD_KT, IRQ11, 0);
SETGATE(idt[IRQ_OFFSET+12], 0, GD_KT, IRQ12, 0);
SETGATE(idt[IRQ_OFFSET+13], 0, GD_KT, IRQ13, 0);
SETGATE(idt[IRQ_OFFSET+14], 0, GD_KT, IRQ14, 0);
SETGATE(idt[IRQ_OFFSET+15], 0, GD_KT, IRQ15, 0);
// Per-CPU setup
还需要开启这个中断,在env_alloc
里面。1
2
3// Enable interrupts while in user mode.
// LAB 4: Your code here.
e->env_tf.tf_eflags |= FL_IF;
Handling Clock Interrupts
在user/spin
程序中,子进程开始运行之后就进入死循环,内核不会再获取控制权。我们现在需要对硬件编程以每隔一定的时间生成时钟中断,这样会强制将控制权返回给内核,内核可以切换到不同的用户环境上运行。
i386_init()
函数调用lapic_init
和pic_init
,设置时钟以及中断控制器生成中断,现在需要编写代码处理这些中断。
这个时候材质 lapic_init
和pic_init
是用来干啥的。
后来发现lapicw(TICR, 10000000);
这个是设置中断时间具体细节就不知道了,应该和嵌入式有关。练习 14
让我们trap_dispatch
里面实现调度,也就是抢占式调度。1
2
3
4
5case IRQ_OFFSET + IRQ_TIMER:{
lapic_eoi();
sched_yield();
break;
}
Inter-Process communication (IPC)
最后一个就是进程通信。我们到目前为止,都是假装一个电脑就只有一个进程,现在我们要开始烤炉两个进程之间的相互影响。我们需要实现一个简单的进程通信。
IPC in JOS
我们需要实现两个系统调用sys_ipc_recv
和 sys_ipc_try_send
并且我们已经用ipc_recv
和ipc_send
封装好了他(C语言里面有封装的概念??),我们发送的信息是一个32位的值和可选的一个单页映射。
允许用户环境在消息中传递页面映射提供了一种传输更多的数据的有效的方法,而不仅仅是单个32位整数,并且还允许用户环境轻松地建立共享内存布局。
Sending and Receiving Messages
用户环境调用sys_ipc_recv
接收消息。此系统调用会调度当前环境,使得在收到消息之前不再运行它。当用户环境等待接收消息时,任何其他用户环境都可以向其发送消息– 而不仅仅是特定的环境,而不仅仅是与接收消息的用户环境具有的父/子关系的用户环境。换而言之,在PartA中实现的权限检查不再适用于IPC,因为IPC系统调用是精心设计的,以便是“安全的”:用户环境不能仅仅通过发送消息而导致另一个环境故障(除非目标环境也是错误的)。
用户环境以接收消息的用户环境的id以及待发送的值为参数调用sys_ipc_try_send
发送一个值。如果接收消息的用户环境是否正在接收消息(该用户环境调用sys_ipc_recv
系统调用,但还没有接收到值),sys_ipc_try_send
系统调用传送消息并返回0,否则返回-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
(在UTO
P下面)以及权限perm
为参数调用sys_ipc_try_send
时,这意味着发送者想要将当前映射到srcva
的页面发送给接收者。在成功的IPC
之后,发送方在其地址空间中的srcva
保持页面的原始映射,但是接收方在其地址空间最初指定的dstva
处获得了与发送者同一物理页的映射。因此,该页面在发送者和接收者之间共享。
如果发送者或接收者没有指示一个页面应该被传送,那么没有页面被传送。在任何IPC
之后,内核将接收者的Env
结构中的新字段env_ipc_perm
设置为接收到的页面的权限,如果没有接收到页面,则为零。
Implementing IPC
介绍了这么多东西其实也就是为了最后这个。练习15
实现sys_ipc_recv
和sys_ipc_recv
。
sys_ipc_recv
1 | // Try to send 'value' to the target env 'envid'. |
sys_ipc_recv
1 |
|
最后别忘了,syscall
1
2
3
4
5case SYS_ipc_try_send:
return sys_ipc_try_send((envid_t)a1, (uint32_t)a2, (void *)a3, (unsigned)a4);
case SYS_ipc_recv:
return sys_ipc_recv((void *)a1);
default:
然后让我们实现lib/ipc.c
里面的ipc_recv
和ipc_send
。
ipc.c
1 | // User-level IPC library routines |