MIT6.828(四)Lab4:Preemptive Multitasking

为了方便查看最终源码,我将代码放到了我的github上。后面会把前面lab的也会添加进去。lab4有很多细节,所以有些东西我会分一下测试程序。

Lab 4: Preemptive Multitasking

  • PartA:
    JOS增添多处理器支持特性。
    实现round-robin scheduling循环调度。
    添加一个基本的环境(进程)管理系统调用(创建和销毁环境,分配和映射内存)。
  • PartB:
    实现一个类Unixfork(),其允许一个用户模式的环境能创建一份它自身的拷贝。
  • PartC:

    支持进程间通信(inter-process communication, IPC
    支持硬件时钟中断和抢占

做个大致介绍让你明白要做啥。
然后就让你切换到lab4,每个lab 必须做的事情,然后会多出来一些文件。每个文件的作用看翻译应该就能明白,在我的github每个文件最前面也有注释。

Part A: Multiprocessor Support and Cooperative Multitasking

先是一堆介绍,就是告诉你要实现轮转调度。后面会为你实现抢占式调度。还有要多CPU 支持。

Multiprocessor Support

我们将让 JOS 支持对称多处理器(symmetric multiprocessingSMP),具体是什么东西自己去看讲操作系统的书。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
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// The local APIC manages internal (non-I/O) interrupts.
// See Chapter 8 & Appendix C of Intel processor manual volume 3.
/*
Kernel code driving the local APIC unit in each processor
内核代码 用来读取每个处理器地方APIC单元。不知道是用来干啥的
*/
#include <inc/types.h>
#include <inc/memlayout.h>
#include <inc/trap.h>
#include <inc/mmu.h>
#include <inc/stdio.h>
#include <inc/x86.h>
#include <kern/pmap.h>
#include <kern/cpu.h>

// Local APIC registers, divided by 4 for use as uint32_t[] indices. // LAPIC 寄存器 用4分成一个个索引。
#define ID (0x0020/4) // ID
#define VER (0x0030/4) // Version
#define TPR (0x0080/4) // Task Priority
#define EOI (0x00B0/4) // EOI
#define SVR (0x00F0/4) // Spurious Interrupt Vector 伪中断向量
#define ENABLE 0x00000100 // Unit Enable 单元可用
#define ESR (0x0280/4) // Error Status 错误信息
#define ICRLO (0x0300/4) // Interrupt Command
#define INIT 0x00000500 // INIT/RESET 初始化
#define STARTUP 0x00000600 // Startup IPI 开始IPI
#define DELIVS 0x00001000 // Delivery status
#define ASSERT 0x00004000 // Assert interrupt (vs deassert)
#define DEASSERT 0x00000000
#define LEVEL 0x00008000 // Level triggered
#define BCAST 0x00080000 // Send to all APICs, including self.
#define OTHERS 0x000C0000 // Send to all APICs, excluding self.
#define BUSY 0x00001000
#define FIXED 0x00000000
#define ICRHI (0x0310/4) // Interrupt Command [63:32]
#define TIMER (0x0320/4) // Local Vector Table 0 (TIMER)
#define X1 0x0000000B // divide counts by 1
#define PERIODIC 0x00020000 // Periodic
#define PCINT (0x0340/4) // Performance Counter LVT
#define LINT0 (0x0350/4) // Local Vector Table 1 (LINT0)
#define LINT1 (0x0360/4) // Local Vector Table 2 (LINT1)
#define ERROR (0x0370/4) // Local Vector Table 3 (ERROR)
#define MASKED 0x00010000 // Interrupt masked
#define TICR (0x0380/4) // Timer Initial Count
#define TCCR (0x0390/4) // Timer Current Count
#define TDCR (0x03E0/4) // Timer Divide Configuration

physaddr_t lapicaddr; // Initialized in mpconfig.c
volatile uint32_t *lapic;

static void
lapicw(int index, int value)
{
lapic[index] = value;
lapic[ID]; // wait for write to finish, by reading
}

void
lapic_init(void) //这个到很part C 才会用到,用于抢占式调度
{
if (!lapicaddr)
return;

// lapicaddr is the physical address of the LAPIC's 4K MMIO //映射这个地址能让我用虚拟地址访问
// region. Map it in to virtual memory so we can access it.
lapic = mmio_map_region(lapicaddr, 4096);

// Enable local APIC; set spurious interrupt vector. 开启 伪中断
lapicw(SVR, ENABLE | (IRQ_OFFSET + IRQ_SPURIOUS));

// The timer repeatedly counts down at bus frequency
// from lapic[TICR] and then issues an interrupt.
// If we cared more about precise timekeeping, //重负时间中断,可以用外面时钟来校准
// TICR would be calibrated using an external time source.
lapicw(TDCR, X1);
lapicw(TIMER, PERIODIC | (IRQ_OFFSET + IRQ_TIMER));
lapicw(TICR, 10000000);

// Leave LINT0 of the BSP enabled so that it can get
// interrupts from the 8259A chip.
//不懂啥意思
// According to Intel MP Specification, the BIOS should initialize
// BSP's local APIC in Virtual Wire Mode, in which 8259A's
// INTR is virtually connected to BSP's LINTIN0. In this mode,
// we do not need to program the IOAPIC.
if (thiscpu != bootcpu)
lapicw(LINT0, MASKED);

// Disable NMI (LINT1) on all CPUs 这个也不知道
lapicw(LINT1, MASKED);

// Disable performance counter overflow interrupts
// on machines that provide that interrupt entry.
if (((lapic[VER]>>16) & 0xFF) >= 4)
lapicw(PCINT, MASKED);

// Map error interrupt to IRQ_ERROR. 映射错误中断
lapicw(ERROR, IRQ_OFFSET + IRQ_ERROR);

// Clear error status register (requires back-to-back writes). 清楚寄存器
lapicw(ESR, 0);
lapicw(ESR, 0);

// Ack any outstanding interrupts.
lapicw(EOI, 0);

// Send an Init Level De-Assert to synchronize arbitration ID's.
lapicw(ICRHI, 0);
lapicw(ICRLO, BCAST | INIT | LEVEL);
while(lapic[ICRLO] & DELIVS)
;

// Enable interrupts on the APIC (but not on the processor).启用 中断
lapicw(TPR, 0);
}

int
cpunum(void) //这个用到的非常多,返回当前CPU是第几个
{
if (lapic)
return lapic[ID] >> 24;
return 0;
}

// Acknowledge interrupt. 确认中断,没怎么用到
void
lapic_eoi(void)
{
if (lapic)
lapicw(EOI, 0);
}

// Spin for a given number of microseconds.
// On real hardware would want to tune this dynamically.
static void
microdelay(int us)
{
}

#define IO_RTC 0x70

// Start additional processor running entry code at addr. 启动额外处理器 后面就会用到
// See Appendix B of MultiProcessor Specification.
void
lapic_startap(uint8_t apicid, uint32_t addr)
{
int i;
uint16_t *wrv;

// "The BSP must initialize CMOS shutdown code to 0AH
// and the warm reset vector (DWORD based at 40:67) to point at
// the AP startup code prior to the [universal startup algorithm]."
outb(IO_RTC, 0xF); // offset 0xF is shutdown code
outb(IO_RTC+1, 0x0A);
wrv = (uint16_t *)KADDR((0x40 << 4 | 0x67)); // Warm reset vector
wrv[0] = 0;
wrv[1] = addr >> 4;

// "Universal startup algorithm."
// Send INIT (level-triggered) interrupt to reset other CPU.
lapicw(ICRHI, apicid << 24);
lapicw(ICRLO, INIT | LEVEL | ASSERT);
microdelay(200);
lapicw(ICRLO, INIT | LEVEL);
microdelay(100); // should be 10ms, but too slow in Bochs!

// Send startup IPI (twice!) to enter code.
// Regular hardware is supposed to only accept a STARTUP
// when it is in the halted state due to an INIT. So the second
// should be ignored, but it is part of the official Intel algorithm.
// Bochs complains about the second one. Too bad for Bochs.
for (i = 0; i < 2; i++) {
lapicw(ICRHI, apicid << 24);
lapicw(ICRLO, STARTUP | (addr >> 12));
microdelay(200);
}
}

void
lapic_ipi(int vector)
{
lapicw(ICRLO, OTHERS | FIXED | vector);
while (lapic[ICRLO] & DELIVS)
;
}

看了上面的,其实啥都不知道,就耽误了下时间。。。。
LAPIChole 开始于物理地址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
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
void
i386_init(void)
{
// Initialize the console.
// Can't call cprintf until after we do this!
cons_init();

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();//初始化自己lapic 这个时候其他CPU还没有启动,此时还是BSP

// 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*
// Schedule and run the first user environment!开始调度
sched_yield();
}

看完后我们去看看mp_init()。并没有要我们实现,过一眼就行了,由于注释太少,所以没看懂。知道他把所有CPU信息收集完就行了。
boot_aps() 是引导其他CPU启动的。他和bootloader 差不多。所以他也是从实模式开始的。我们将kern/mpentry.S加载到0x7000很眼熟…,好像只要是没有用的页对齐的低于640的地址都可以。
然后欧美就去看看他做了啥。

boot_aps

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
// Start the non-boot (AP) processors.
static void
boot_aps(void)
{
extern unsigned char mpentry_start[], mpentry_end[]; //那段代码的开始和结束
void *code;
struct CpuInfo *c;

// Write entry code to unused memory at MPENTRY_PADDR #define MPENTRY_PADDR 0x7000
code = KADDR(MPENTRY_PADDR); //找到这个地址 在memlayout 里面宏定义了。
memmove(code, mpentry_start, mpentry_end - mpentry_start);//把代码复制进去

// Boot each AP one at a time
for (c = cpus; c < cpus + ncpu; c++) { //CPUS 是啥 NCPU 是啥??? 就是前面那个我们没有讲的文件里面收集的,cpus 是 所有CPUS的数组,ncpu 是个数
if (c == cpus + cpunum()) // We've started already. 不是自己这个CPU
continue;

// Tell mpentry.S what stack to use 这个是每个CPU的栈,现在还没有映射,等会要做的就是这个
mpentry_kstack = percpu_kstacks[c - cpus] + KSTKSIZE;
// Start the CPU at mpentry_start
lapic_startap(c->cpu_id, PADDR(code)); //这个就是开始启动CPUS4了
// Wait for the CPU to finish some basic setup in mp_main()
while(c->cpu_status != CPU_STARTED) //等待这个CPU启动玩
;
//在 mp_main里有这么一行 xchg(&thiscpu->cpu_status, CPU_STARTED); // tell boot_aps() we're up
}
}

然后我们去看看mp_main()在哪进入的。我们能够看出boot_aps,让我慢慢跑去了mpentry_start

mpentry_start

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
/* See COPYRIGHT for copyright information. */
/*Assembly-language entry code for non-boot CPUs 汇编代码启动 非引导CPU 应该不会要动,这种东西一般都是固定的*/
#include <inc/mmu.h>
#include <inc/memlayout.h>

###################################################################
# entry point for APs
###################################################################

# Each non-boot CPU ("AP") is started up in response to a STARTUP
# IPI from the boot CPU. Section B.4.2 of the Multi-Processor
# Specification says that the AP will start in real mode with CS:IP
# set to XY00:0000, where XY is an 8-bit value sent with the
# STARTUP. Thus this code must start at a 4096-byte boundary.
# 代码必须从4096开始运行,和那个bootloader 没多大差距,除了我们能控制在哪运行
# Because this code sets DS to zero, it must run from an address in
# the low 2^16 bytes of physical memory.
#
# boot_aps() (in init.c) copies this code to MPENTRY_PADDR (which
# satisfies the above restrictions). Then, for each AP, it stores the
# address of the pre-allocated per-core stack in mpentry_kstack, sends
# the STARTUP IPI, and waits for this code to acknowledge that it has
# started (which happens in mp_main in init.c).
#
# This code is similar to boot/boot.S except that
# - it does not need to enable A20
# - it uses MPBOOTPHYS to calculate absolute addresses of its
# symbols, rather than relying on the linker to fill them

#define RELOC(x) ((x) - KERNBASE)
#define MPBOOTPHYS(s) ((s) - mpentry_start + MPENTRY_PADDR) //这个是因为BSP已经是在保护模式下了没法直接访问,当前CPU又在实模式,是不能访问呢 1M以上的内存,所以要映射一下。

.set PROT_MODE_CSEG, 0x8 # kernel code segment selector
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector

.code16
.globl mpentry_start
mpentry_start:
cli

xorw %ax, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss

lgdt MPBOOTPHYS(gdtdesc)
movl %cr0, %eax
orl $CR0_PE, %eax
movl %eax, %cr0

ljmpl $(PROT_MODE_CSEG), $(MPBOOTPHYS(start32))

.code32
start32:
movw $(PROT_MODE_DSEG), %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
movw $0, %ax
movw %ax, %fs
movw %ax, %gs

# Set up initial page table. We cannot use kern_pgdir yet because
# we are still running at a low EIP.
movl $(RELOC(entry_pgdir)), %eax
movl %eax, %cr3
# Turn on paging.
movl %cr0, %eax
orl $(CR0_PE|CR0_PG|CR0_WP), %eax
movl %eax, %cr0

# Switch to the per-cpu stack allocated in boot_aps()
movl mpentry_kstack, %esp
movl $0x0, %ebp # nuke frame pointer

# Call mp_main(). (Exercise for the reader: why the indirect call?) 在这个地方我们跑去了 mp_main
movl $mp_main, %eax
call *%eax

# If mp_main returns (it shouldn't), loop.
spin:
jmp spin

# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULL # null seg
SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg
SEG(STA_W, 0x0, 0xffffffff) # data seg

gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long MPBOOTPHYS(gdt) # address gdt

.globl mpentry_end
mpentry_end:
nop

再看看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

  1. 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
多处理器支持的私有内核定义
应该是 定义了 多处理器的一些操作
*/
#ifndef JOS_INC_CPU_H
#define JOS_INC_CPU_H

#include <inc/types.h>
#include <inc/memlayout.h>
#include <inc/mmu.h>
#include <inc/env.h>

// Maximum number of CPUs
#define NCPU 8

// 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
#define thiscpu (&cpus[cpunum()]) //指向自己这个CPU

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);//不知道是啥

#endif

每个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的TSScpus[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
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
// Modify mappings in kern_pgdir to support SMP
// - Map the per-CPU stacks in the region [KSTACKTOP-PTSIZE, KSTACKTOP)
//映射去支持SMP 映射地址是 [KSTACKTOP-PTSIZE, KSTACKTOP)
static void
mem_init_mp(void)
{
// Map per-CPU stacks starting at KSTACKTOP, for up to 'NCPU' CPUs.
//映射地址从 KSTACKTOP 开始
// For CPU i, use the physical memory that 'percpu_kstacks[i]' refers 每个CPU i 的物理地址是 'percpu_kstacks[i]
// to as its kernel stack. CPU i's kernel stack grows down from virtual
// address kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP), and is 开始地址是 kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP)
// divided into two pieces, just like the single stack you set up in
// mem_init: 这个是不是很眼熟,和前面单CPU是一样的 有一块是不用映射的,这样如果栈溢出,就直接RE
// * [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:
for (size_t i = 0; i < NCPU; i++)
{
/* code 直接映射即可 */
boot_map_region(kern_pgdir,KSTACKTOP-i*(KSTKSIZE+KSTKGAP)-KSTKSIZE,KSTKSIZE,PADDR(percpu_kstacks[i]),PTE_W);
}

}

练习 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
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
// Mutual exclusion spin locks.
/* 头文件 介绍过了 ,这个也不用多说了吧*/
#include <inc/types.h>
#include <inc/assert.h>
#include <inc/x86.h>
#include <inc/memlayout.h>
#include <inc/string.h>
#include <kern/cpu.h>
#include <kern/spinlock.h>
#include <kern/kdebug.h>

// The big kernel lock
struct spinlock kernel_lock = {
#ifdef DEBUG_SPINLOCK
.name = "kernel_lock"
#endif
};

#ifdef DEBUG_SPINLOCK
// Record the current call stack in pcs[] by following the %ebp chain. 这个不知道用来噶啥的没用熬过
static void
get_caller_pcs(uint32_t pcs[])
{
uint32_t *ebp;
int i;

ebp = (uint32_t *)read_ebp();
for (i = 0; i < 10; i++){
if (ebp == 0 || ebp < (uint32_t *)ULIM)
break;
pcs[i] = ebp[1]; // saved %eip
ebp = (uint32_t *)ebp[0]; // saved %ebp
}
for (; i < 10; i++)
pcs[i] = 0;
}

// Check whether this CPU is holding the lock. 检查当前CPU是否有锁
static int
holding(struct spinlock *lock)
{
return lock->locked && lock->cpu == thiscpu;
}
#endif

void
__spin_initlock(struct spinlock *lk, char *name)//初始化锁的样子
{
lk->locked = 0;
#ifdef DEBUG_SPINLOCK
lk->name = name;
lk->cpu = 0;
#endif
}

// Acquire the lock.
// Loops (spins) until the lock is acquired.
// Holding a lock for a long time may cause
// other CPUs to waste time spinning to acquire it.//锁住内核
void
spin_lock(struct spinlock *lk)
{
#ifdef DEBUG_SPINLOCK
if (holding(lk))
panic("CPU %d cannot acquire %s: already holding", cpunum(), lk->name);
#endif

// The xchg is atomic.
// It also serializes, so that reads after acquire are not
// reordered before it.
while (xchg(&lk->locked, 1) != 0) //如果是已经锁住的,就一直等待
asm volatile ("pause");

// Record info about lock acquisition for debugging.
#ifdef DEBUG_SPINLOCK
lk->cpu = thiscpu; //锁住的CPU变成自己
get_caller_pcs(lk->pcs);
#endif
}

// Release the lock. 解除锁
void
spin_unlock(struct spinlock *lk)
{
#ifdef DEBUG_SPINLOCK
if (!holding(lk)) {
int i;
uint32_t pcs[10];
// Nab the acquiring EIP chain before it gets released
memmove(pcs, lk->pcs, sizeof pcs);
cprintf("CPU %d cannot release %s: held by CPU %d\nAcquired at:",
cpunum(), lk->name, lk->cpu->cpu_id);
for (i = 0; i < 10 && pcs[i]; i++) {
struct Eipdebuginfo info;
if (debuginfo_eip(pcs[i], &info) >= 0)
cprintf(" %08x %s:%d: %.*s+%x\n", pcs[i],
info.eip_file, info.eip_line,
info.eip_fn_namelen, info.eip_fn_name,
pcs[i] - info.eip_fn_addr);
else
cprintf(" %08x\n", pcs[i]);
}
panic("spin_unlock");
}

lk->pcs[0] = 0;
lk->cpu = 0;
#endif

// The xchg instruction is atomic (i.e. uses the "lock" prefix) with
// respect to any other instruction which references the same memory.
// x86 CPUs will not reorder loads/stores across locked instructions
// (vol 3, 8.2.2). Because xchg() is implemented using asm volatile,
// gcc will not reorder C statements across the xchg.
xchg(&lk->locked, 0);//释放内核
}

里面用的上的函数,也就两个spin_lockspin_unlock,他们在spinlock.h里面用lock_kernelunlock_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
6
if ((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
2
unlock_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
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

// 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. //永远不会运行其他CPU 上正在运行的环境,如果没有可以运行的CPU 就是停止 这个CPU
// LAB 4: Your code here.
int i, nxenvid;
if (curenv)
nxenvid = ENVX(curenv->env_id);
else
nxenvid = 0; //如果是第一调度是 0
//枚举所有进程,看有没有能够运行的,有的运行。
for (i = 0; i < NENV; i++) {
if (envs[(nxenvid + i) % NENV].env_status == ENV_RUNNABLE){
envs[(nxenvid + i) % NENV].env_cpunum=cpunum();
env_run(&envs[(nxenvid + i) % NENV]);
}
}
if (curenv && curenv->env_status == ENV_RUNNING){//没有其他的就运行当前的环境
curenv->env_cpunum=cpunum();
env_run(curenv);
}

// sched_halt never returns 当前环境如果都不可运行了就直接 停止CPU
sched_halt();
}

实现了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

#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);//把这个歌注释掉,添加下面 3个进程
ENV_CREATE(user_yield, ENV_TYPE_USER);
ENV_CREATE(user_yield, ENV_TYPE_USER);
ENV_CREATE(user_yield, ENV_TYPE_USER);

#endif // TEST*

然后运行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

#include <inc/lib.h>

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.

#include <inc/string.h>
#include <inc/lib.h>

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;
}

所以说说上面就是判断一下进程是不是可用的。如果chekperm1还需要检查是不是当前进程是不是当前进程或子进程。
练习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_allocsys_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
10
case 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
2
case 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
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
void
page_fault_handler(struct Trapframe *tf)
{
uint32_t fault_va;

// Read processor's CR2 register to find the faulting address
fault_va = rcr2();

// Handle kernel-mode page faults.
// LAB 3: Your code here.
if(tf->tf_cs && 0x01 == 0) {
panic("page_fault in kernel mode, fault address %d\n", fault_va);
}

// We've already handled kernel-mode exceptions, so if we get here,
// the page fault happened in user mode.

// Call the environment's page fault upcall, if one exists. Set up a
// page fault stack frame on the user exception stack (below
// UXSTACKTOP), then branch to curenv->env_pgfault_upcall.
// 建立一个用户异常栈 在 UXSTACKTOP 然后跳转到 curenv->env_pgfault_upcall 运行
// The page fault upcall might cause another page fault, in which case
// we branch to the page fault upcall recursively, pushing another
// page fault stack frame on top of the user exception stack.
// 可能出现多级中断
// It is convenient for our code which returns from a page fault
// (lib/pfentry.S) to have one word of scratch space at the top of the
// trap-time stack; it allows us to more easily restore the eip/esp. In
// the non-recursive case, we don't have to worry about this because
// the top of the regular user stack is free. In the recursive case,
// this means we have to leave an extra word between the current top of
// the exception stack and the new stack frame because the exception
// stack _is_ the trap-time stack.
//
// If there's no page fault upcall, the environment didn't allocate a
// page for its exception stack or can't write to it, or the exception
// stack overflows, then destroy the environment that caused the fault.
// Note that the grade script assumes you will first check for the page
// fault upcall and print the "user fault va" message below if there is
// none. The remaining three checks can be combined into a single test.
//
// Hints:
// user_mem_assert() and env_run() are useful here.
// To change what the user environment runs, modify 'curenv->env_tf'
// (the 'tf' variable points at 'curenv->env_tf').

// LAB 4: Your code here.
struct UTrapframe *utf;

if (curenv->env_pgfault_upcall) {
//如果已经有了异常栈,我们就直接在后面添加一个UTrapframe,否则就先把跳到异常栈。 这是为了处理多级中断
if (tf->tf_esp >= UXSTACKTOP-PGSIZE && tf->tf_esp < UXSTACKTOP) {
// 异常模式下陷入
utf = (struct UTrapframe *)(tf->tf_esp - sizeof(struct UTrapframe) - 4);
}
else {
// 非异常模式下陷入
utf = (struct UTrapframe *)(UXSTACKTOP - sizeof(struct UTrapframe));
}
// 检查异常栈是否溢出
user_mem_assert(curenv, (const void *) utf, sizeof(struct UTrapframe), PTE_P|PTE_W);

utf->utf_fault_va = fault_va;
utf->utf_err = tf->tf_trapno;
utf->utf_regs = tf->tf_regs;
utf->utf_eflags = tf->tf_eflags;
// 保存陷入时现场,用于返回
utf->utf_eip = tf->tf_eip;
utf->utf_esp = tf->tf_esp;
// 再次转向执行
curenv->env_tf.tf_eip = (uint32_t) curenv->env_pgfault_upcall;
// 异常栈
curenv->env_tf.tf_esp = (uint32_t) utf;
env_run(curenv);
}
else {
// Destroy the environment that caused the fault. 没有定义就直接销毁
cprintf("[%08x] user fault va %08x ip %08x\n",
curenv->env_id, fault_va, tf->tf_eip);
print_trapframe(tf);
env_destroy(curenv);
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//
// Set the page fault handler function. 设置那个缺页处理
// If there isn't one yet, _pgfault_handler will be 0.
// The first time we register a handler, we need to
// allocate an exception stack (one page of memory with its top
// at UXSTACKTOP), and tell the kernel to call the assembly-language
// _pgfault_upcall routine when a page fault occurs. 如果是第一次分配页我们需要分配一个异常栈。
//
void
set_pgfault_handler(void (*handler)(struct UTrapframe *utf))
{
int r;

if (_pgfault_handler == 0) {
// First time through!
// LAB 4: Your code here.
if ((r = sys_page_alloc(thisenv->env_id, (void *)(UXSTACKTOP - PGSIZE), PTE_P | PTE_W | PTE_U)) < 0)
panic("set_pgfault_handler: %e", r);
sys_env_set_pgfault_upcall(thisenv->env_id, _pgfault_upcall);
}

// Save handler pointer for assembly to call.
_pgfault_handler = handler;
}

Implementing Copy-on-Write Fork

最后就是实现写时复制了。
前面我们有一个测试程序,user/dumbfork,这个里面已经有了模板,我们现在要做的就是实现一个差不多的fork
他的基本流程是:

  1. 父进程将pgfault()函数作为C语言实现的页错误处理,会用到上面的实现的set_pgfault_handler()函数进行设置。父进程调用sys_exofork()创建一个子进程环境。
  2. UTOP之下的在地址空间里的每一个可写或copy-on-write的页,父进程就会调用duppage,它会将copy-on-write页映射到子进程的地址空间,然后重新映射copy-on-write页到自己的地址空间。[注意这里的顺序十分重要!先将子进程的页标记为COW,然后将父进程的页标记为COW。知道为什么吗?你可以尝试思考将该顺序弄反会是造成怎样的麻烦]。duppageCOW的页的PTEs设置为不能写的,然后在PTEavail域设置PTE_COW来区别 copy-on-write pages及真正的只读页
  3. 异常栈并不是如上重新映射,在子进程中需要为异常栈分配一个新的页。由于缺页异常处理程序将执行实际的拷贝,而且缺页异常处理程序在异常栈上运行,异常栈不应该被设置为cowfork()同样要解决在内存中的页,但页既不可写也不是copy-on-write
  4. 父进程为子进程设置用户页错误入口。
  5. 子进程现在可以运行,然后父进程将其标记为可运行。

每次这两进程中的一个向一个尚未写过的copy-on-write页写时,就会产生一个页错误。下面是用户页错误处理的控制流:

  1. 内核传播页错误到_pgfault_upcall,调用fork()pgfault()处理流程。
  2. pgfault()检查错误代码中的FEC_WR(即是写导致的),以及页对应的PTE标记为PTE_COW。没有的话,panic
  3. pgfault()分配一个映射在一个临时位置的新的页,然后将错误页中的内容复制进去。然后页错误处理程序映射新的页到引起page fault的虚拟地址,并设置PTE具有读写权限。

用户级的lib/fork.c必须访问用户环境的页表完成以上的几个操作(例如将一个页对应的PTE标记为PTE_COW)。内核映射用户环境的页表到虚拟地址UVPT的用意就在于此。它使用了一种聪明的手段让用户代码很方便的检索PTElib/entry.S设置uvptuvpd使得lib/fork.c中的用户代码能够轻松地检索页表信息。
练习12就是让我们实现fork.c里面的fork, duppagepgfault

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

#include <inc/string.h>
#include <inc/lib.h>

// PTE_COW marks copy-on-write page table entries.
// It is one of the bits explicitly allocated to user processes (PTE_AVAIL).
#define PTE_COW 0x800

//
// 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.

#include <inc/lib.h>

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_initpic_init,设置时钟以及中断控制器生成中断,现在需要编写代码处理这些中断。
这个时候材质 lapic_initpic_init 是用来干啥的。
后来发现lapicw(TICR, 10000000);这个是设置中断时间具体细节就不知道了,应该和嵌入式有关。
练习 14让我们trap_dispatch里面实现调度,也就是抢占式调度。

1
2
3
4
5
case IRQ_OFFSET + IRQ_TIMER:{
lapic_eoi();
sched_yield();
break;
}

Inter-Process communication (IPC)

最后一个就是进程通信。我们到目前为止,都是假装一个电脑就只有一个进程,现在我们要开始烤炉两个进程之间的相互影响。我们需要实现一个简单的进程通信。

IPC in JOS

我们需要实现两个系统调用sys_ipc_recvsys_ipc_try_send并且我们已经用ipc_recvipc_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(在UTOP下面)以及权限perm为参数调用sys_ipc_try_send时,这意味着发送者想要将当前映射到srcva的页面发送给接收者。在成功的IPC之后,发送方在其地址空间中的srcva保持页面的原始映射,但是接收方在其地址空间最初指定的dstva处获得了与发送者同一物理页的映射。因此,该页面在发送者和接收者之间共享。

如果发送者或接收者没有指示一个页面应该被传送,那么没有页面被传送。在任何IPC之后,内核将接收者的Env结构中的新字段env_ipc_perm设置为接收到的页面的权限,如果没有接收到页面,则为零。

Implementing IPC

介绍了这么多东西其实也就是为了最后这个。
练习15 实现sys_ipc_recvsys_ipc_recv

sys_ipc_recv

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
// Try to send 'value' to the target env 'envid'.
// If srcva < UTOP, then also send page currently mapped at 'srcva',
// so that receiver gets a duplicate mapping of the same page.
// 发过 srcva < UTOP 把他对应的物理页送过去
// The send fails with a return value of -E_IPC_NOT_RECV if the
// target is not blocked, waiting for an IPC.
// 失败返回 E_IPC_NOT_RECV 如果目标是不堵塞的就等待IPC
// The send also can fail for the other reasons listed below.
//
// Otherwise, the send succeeds, and the target's ipc fields are
// updated as follows: //如果发送成功更新下面这些东西
// env_ipc_recving is set to 0 to block future sends; 设置 0来阻塞
// env_ipc_from is set to the sending envid;//设置发送的ID
// env_ipc_value is set to the 'value' parameter; 设置值
// env_ipc_perm is set to 'perm' if a page was transferred, 0 otherwise.设置发送页面权限
// The target environment is marked runnable again, returning 0 标记目标环境为runnable
// from the paused sys_ipc_recv system call. (Hint: does the
// sys_ipc_recv function ever actually return?)暂停sys_ipc_recv
//
// If the sender wants to send a page but the receiver isn't asking for one,
// then no page mapping is transferred, but no error occurs.
// The ipc only happens when no errors occur.
// 如果当前进程发送了一个页,但是 目标进程没有要求一个页,然后没有 页会被发送,不会产生错误。
// Returns 0 on success, < 0 on error.
// Errors are:
// -E_BAD_ENV if environment envid doesn't currently exist.
// (No need to check permissions.)//如果进程错误
// -E_IPC_NOT_RECV if envid is not currently blocked in sys_ipc_recv,
// or another environment managed to send first.//目标没有接受
// -E_INVAL if srcva < UTOP but srcva is not page-aligned.//页地址错误
// -E_INVAL if srcva < UTOP and perm is inappropriate//页权限错误
// (see sys_page_alloc).
// -E_INVAL if srcva < UTOP but srcva is not mapped in the caller's
// address space.//页没有映射
// -E_INVAL if (perm & PTE_W), but srcva is read-only in the
// current environment's address space.//页只读
// -E_NO_MEM if there's not enough memory to map srcva in envid's
// address space.//没有足够的空间
static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
// LAB 4: Your code here.
struct Env* env;
if(envid2env(envid,&env,0)<0)return -E_BAD_ENV;//环境
if(env->env_ipc_recving==0)return -E_IPC_NOT_RECV;//是否阻塞
env->env_ipc_perm = 0;//权限
unsigned flag= PTE_P | PTE_U;
if((uintptr_t)srcva<UTOP){//页地址小于UTOP
if(PGOFF(srcva))return -E_INVAL;//不是页对齐
if ((perm & ~(PTE_SYSCALL)) || ((perm & flag) != flag))return -E_INVAL;//权限错误
if (user_mem_check(curenv, (const void *)srcva, PGSIZE, PTE_U) < 0)//页不存在
return -E_INVAL;
if (perm& PTE_W&&user_mem_check(curenv, (const void *)srcva, PGSIZE, PTE_U |PTE_W) < 0)//页权限错误
return -E_INVAL;
if((uintptr_t)(env->env_ipc_dstva)<UTOP){//接受页的地址
env->env_ipc_perm=perm;
struct PageInfo *pi = page_lookup(curenv->env_pgdir, srcva, 0);
if (page_insert(env->env_pgdir, pi, env->env_ipc_dstva, perm) < 0)//映射一下,映射失败返回没有内存了。
return -E_NO_MEM;
}
}
//设置一下值
env->env_ipc_recving = false;
env->env_ipc_from = curenv->env_id;
env->env_ipc_value = value;
env->env_status = ENV_RUNNABLE;
env->env_tf.tf_regs.reg_eax = 0;
return 0;
//panic("sys_ipc_try_send not implemented");
}

sys_ipc_recv

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

// Block until a value is ready. Record that you want to receive
// using the env_ipc_recving and env_ipc_dstva fields of struct Env,
// mark yourself not runnable, and then give up the CPU.
// 在等到接受之前一直 阻塞,释放CPU
// If 'dstva' is < UTOP, then you are willing to receive a page of data.
// 'dstva' is the virtual address at which the sent page should be mapped.
//如果 dstva <UTOP 标示你愿意接受一个页。送过来的页将映射到dstva
// This function only returns on error, but the system call will eventually
// return 0 on success.
// Return < 0 on error. Errors are:
// -E_INVAL if dstva < UTOP but dstva is not page-aligned.//只有这样会保存
static int
sys_ipc_recv(void *dstva)
{
// LAB 4: Your code here.
if((dstva < (void *)UTOP) && PGOFF(dstva))//报错
return -E_INVAL;
curenv->env_ipc_recving = true;
curenv->env_ipc_dstva = dstva;
curenv->env_status = ENV_NOT_RUNNABLE;
sched_yield();
//panic("sys_ipc_recv not implemented");
return 0;
}

最后别忘了,syscall

1
2
3
4
5
case 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_recvipc_send

ipc.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
// User-level IPC library routines

#include <inc/lib.h>

// Receive a value via IPC and return it. 接受一个IPC
// If 'pg' is nonnull, then any page sent by the sender will be mapped at
// that address. 如果pg非空会发送他
// If 'from_env_store' is nonnull, then store the IPC sender's envid in
// *from_env_store. 如果from_env_store非空就会把发送进程的id 放到 from_env_store
// If 'perm_store' is nonnull, then store the IPC sender's page permission
// in *perm_store (this is nonzero iff a page was successfully
// transferred to 'pg'). 如果perm_store非空就会储存页权限在这
// If the system call fails, then store 0 in *fromenv and *perm (if
// they're nonnull) and return the error. 如果错了就会把0存到fromenv和perm如果他们非空。
// Otherwise, return the value sent by the sender 否则返回值
//
// Hint:
// Use 'thisenv' to discover the value and who sent it. 用thisenv发现是谁送的
// If 'pg' is null, pass sys_ipc_recv a value that it will understand
// as meaning "no page". (Zero is not the right value, since that's
// a perfectly valid place to map a page.)
int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
// LAB 4: Your code here.
pg = (pg == NULL ? (void *)UTOP : pg);
int r;
if ((r = sys_ipc_recv(pg)) < 0) {//UTOP 相当于没有地址会返回0
if (from_env_store != NULL)
*from_env_store = 0;
if (perm_store != NULL)
*perm_store = 0;
return r;
}
//如果pg==NULL 后面这两个值都毫无意义
if (from_env_store != NULL)
*from_env_store = thisenv->env_ipc_from;
if (perm_store != NULL)
*perm_store = thisenv->env_ipc_perm;
return thisenv->env_ipc_value;
//panic("ipc_recv not implemented");
return 0;
}

// Send 'val' (and 'pg' with 'perm', if 'pg' is nonnull) to 'toenv'.
// This function keeps trying until it succeeds.
// It should panic() on any error other than -E_IPC_NOT_RECV.
// 如果不是 E_IPC_NOT_RECV 就报错。
// Hint:
// Use sys_yield() to be CPU-friendly.
// If 'pg' is null, pass sys_ipc_try_send a value that it will understand
// as meaning "no page". (Zero is not the right value.)
void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
// LAB 4: Your code here.
int r;
while((r=sys_ipc_try_send(to_env,val, (pg == NULL ? (void *)UTOP : pg),perm))<0){
if(r!=-E_IPC_NOT_RECV)panic("sys_ipc_try_send: %e\n", r);
sys_yield();//释放CPU
}//一直发送直到成功...
//panic("ipc_send not implemented");
}

// Find the first environment of the given type. We'll use this to
// find special environments.
// Returns 0 if no such environment exists.
envid_t
ipc_find_env(enum EnvType type)
{
int i;
for (i = 0; i < NENV; i++)
if (envs[i].env_type == type)
return envs[i].env_id;
return 0;
}