爲了方便查看最終源碼,我將代碼放到了我的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
// 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)
;
}
看了上面的,其實啥都不知道,就耽誤了下時間。。。。
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
很像,照着來就行了。
//
// 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
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
// 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
/* 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
// 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
後面繼續添加幾行就可以了。
// 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
裏面定義了各種我們想要的信息。
/*
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的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
// 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()
。
// 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
// 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_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
裏面
// Acquire the big kernel lock before waking up APs
// Your code here:
lock_kernel();
// Starting non-boot CPUs 在這個啓動之前調用lock_kernel();
boot_aps();
第二個 mp_main
裏面
// 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
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()
這個函數跑用戶態去了,所以要釋放內核。
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
// 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
。然後我們需要使用他。
// 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
最後調用一下注釋掉無線循環。
// Remove this after you finish Exercise 6
//for (;;);
sched_yield();
然後我們需要驗證一下,要在init
裏面添加測試樣例。
#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
// 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
。我們先去看看這個程序做了啥。
// 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
。
//
// 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
// 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
更改進程狀態。
// 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
,都差不多。
// 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
裏面。
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
。
// 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
,我這個地方忘記添加了,找了半天不知道爲啥。
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
裏面的用戶模式的缺頁錯誤,因爲現在我們有了用戶的缺頁處理函數。現在我們如果設置了,缺頁處理函數,就調用缺頁處理函數,沒有我們就銷燬這個進程。
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()
系統調用註冊到內核的處理程序。
這個練習我沒看懂。所以我就直接 貼別人的代碼了
// 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。
//
// 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
。
他的基本流程是:
- 父進程將
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
。
// 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
會死循環。
// 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
要我像當初實現內部中斷一樣,實現這幾個外部中斷。
一樣的沒啥區別。
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
裏面。
// 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
裏面實現調度,也就是搶佔式調度。
case 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
// 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
// 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
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_recv
和ipc_send
。
ipc.c
// 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;
}