Alkaid
2209 字
11 分钟
OS Lab3 - 进程与异常
2024-04-25
OS
/
C

OS Lab3 实验报告#

Alkaid-Zhong
/
BUAA-OS-2024
Waiting for api.github.com...
00K
0K
0K
Waiting...

思考题#

Thinking 3.1#

Thinking 3.1 请结合 MOS 中的页目录自映射应用解释代码中

e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V的含义。

UVPT:用户页表的起始处的内核虚拟地址

PDX(UVPT)UVPT地址在页目录中是第几个(pgdir[PDX(UVPT)]

e->env_pgdir[PDX(UVPT)]:当前进程内存中UVPT地址的页目录项

e->env_pgdir:进程 e 的页目录的内核虚拟地址

PADDR(e->env_pgdir):进程 e 的页目录的物理地址

PADDR(e->env_pgdir) | PTE_V:页目录的物理基地址,加上权限位

整体:当前进程内存中UVPT地址的页目录项 = 页目录的物理基地址,加上权限位

Thinking 3.2#

elf_load_seg 以函数指针的形式,接受外部自定义的回调函数 map_page。 请你找到与之相关的 data 这一参数在此处的来源,并思考它的作用。没有这个参数可不可以?为什么?

// elfloader.c
int elf_load_seg(Elf32_Phdr *ph, const void *bin, elf_mapper_t map_page, void *data);

// env.c
static int load_icode_mapper(
    void *data, 
    u_long va, 
    size_t offset, 
    u_int perm,
    const void *src, 
    size_t len
) {
	struct Env *env = (struct Env *)data;
	...
	return page_insert(env->env_pgdir, env->env_asid, p, va, perm);
}

static void load_icode(struct Env *e, const void *binary, size_t size) {
 	//...
    panic_on(elf_load_seg(ph, binary + ph->p_offset, load_icode_mapper, e));
    //...
}

data 的来源是load_icode传入的进程控制块指针

调用顺序:load_icode -> elf_load_seg -> load_icode_mapper

如果没有dataload_icode_mapper就不能知道当前进程空间的页目录基地址和asid,无法page_insert,所以必须要有这个参数。

Thinking 3.3#

结合 elf_load_seg 的参数和实现,考虑该函数需要处理哪些页面加载的情况。

int elf_load_seg(Elf32_Phdr *ph, const void *bin, elf_mapper_t map_page, void *data) {
	u_long va = ph->p_vaddr;
	size_t bin_size = ph->p_filesz;
	size_t sgsize = ph->p_memsz;
	u_int perm = PTE_V;
    // 先修改后面的权限位
	if (ph->p_flags & PF_W) {
		perm |= PTE_D;
	}

	int r;
	size_t i;
	u_long offset = va - ROUNDDOWN(va, PAGE_SIZE);
    // 如果va没有页面对其,就将多余的地址记为offset
    // 并且将PAGE_SIZE - offset的binary数据,写入对应页的对应地址
	if (offset != 0) {
		if ((r = map_page(data, va, offset, perm, bin,
				  MIN(bin_size, PAGE_SIZE - offset))) != 0) {
			return r;
		}
	}

	/* Step 1: load all content of bin into memory. */
    // 将段内页映射到物理空间
	for (i = offset ? MIN(bin_size, PAGE_SIZE - offset) : 0; i < bin_size; i += PAGE_SIZE) {
		if ((r = map_page(data, va + i, 0, perm, bin + i, MIN(bin_size - i, PAGE_SIZE))) !=
		    0) {
			return r;
		}
	}

	/* Step 2: alloc pages to reach `sgsize` when `bin_size` < `sgsize`. */
    // 把多余的空间用0填充满
	while (i < sgsize) {
		if ((r = map_page(data, va + i, 0, perm, NULL, MIN(sgsize - i, PAGE_SIZE))) != 0) {
			return r;
		}
		i += PAGE_SIZE;
	}
	return 0;
}

Thinking 3.4#

这里的 env_tf.cp0_epc 字段指示了进程恢复运行时 PC 应恢复到的位置。我们要运行的进 程的代码段预先被载入到了内存中,且程序入口为 e_entry,当我们运行进程时,CPU 将自动 从 PC 所指的位置开始执行二进制码。

你认为这里的 env_tf.cp0_epc 存储的是物理地址还是虚拟地址?

虚拟地址

Thinking 3.5#

试找出 0、1、2、3 号异常处理函数的具体实现位置。8 号异常(系统调用) 涉及的 do_syscall() 函数将在 Lab4 中实现。

// traps.c

// 先定义了一些handler
extern void handle_int(void);
extern void handle_tlb(void);
extern void handle_sys(void);
extern void handle_mod(void);
extern void handle_reserved(void);

// 用于分发不用的ExcCode
void (*exception_handlers[32])(void) = {
    [0 ... 31] = handle_reserved,
    [0] = handle_int,
    [2 ... 3] = handle_tlb,
#if !defined(LAB) || LAB >= 4
    [1] = handle_mod,
    [8] = handle_sys,
#endif
};

// 用于处理未知(其实是未定义处理函数)的ExcCode
void do_reserved(struct Trapframe *tf) {
	print_tf(tf);
	panic("Unknown ExcCode %2d", (tf->cp0_cause >> 2) & 0x1f);
}
# genex.S
# 其他的异常处理函数定义在这里
NESTED(handle_int, TF_SIZE, zero)
	mfc0    t0, CP0_CAUSE
	mfc0    t2, CP0_STATUS
	and     t0, t2
	andi    t1, t0, STATUS_IM7
	bnez    t1, timer_irq
timer_irq:
	li      a0, 0
	j       schedule
END(handle_int

BUILD_HANDLER tlb do_tlb_refill

#if !defined(LAB) || LAB >= 4
BUILD_HANDLER mod do_tlb_mod
BUILD_HANDLER sys do_syscall
#endif

BUILD_HANDLER reserved do_reserved

Thinking 3.6#

阅读 entry.S、genex.S 和 env_asm.S 这几个文件,并尝试说出时钟中断在哪些时候开启,在哪些时候关闭。

# 在 entry.S 中
exc_gen_entry: # 进入异常处理
	mfc0    t0, CP0_STATUS
	and     t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE) 
	mtc0    t0, CP0_STATUS # 禁止中断
/* Exercise 3.9: Your code here. */
	mfc0 	t0, CP0_CAUSE
	andi 	t0, 0x7c
	lw		t0, exception_handlers(t0)
	jr 		t0
	
# 在 env_asm.S 中,env_pop_tf函数负责reset时钟
LEAF(env_pop_tf)
.set reorder
.set at
	mtc0    a1, CP0_ENTRYHI
	move    sp, a0
	RESET_KCLOCK
	j       ret_from_exception
END(env_pop_tf)

# 在 genex.S 中
FEXPORT(ret_from_exception)
	RESTORE_ALL 
	eret # 打开中断

Thinking 3.7#

阅读相关代码,思考操作系统是怎么根据时钟中断切换进程的。

在进程运行过程中,若时钟中断产生,则会触发MIPS中断,系统将PC指向 0x800000080,跳转到.text.exc_gen_entry代码段,进行异常分发;

NESTED(handle_int, TF_SIZE, zero)
	mfc0    t0, CP0_CAUSE
	mfc0    t2, CP0_STATUS
	and     t0, t2
	andi    t1, t0, STATUS_IM7
	bnez    t1, timer_irq
timer_irq:
	li      a0, 0
	j       schedule
END(handle_int)

由于是中断,会被分发到handle_int,从而进入schedule,进行调度

schedule函数中

void schedule(int yield) {
	static int count = 0; // remaining time slices of current env
	struct Env *e = curenv; // 先获取当前进程的进程控制块
	count--; // 可用时间片数量减一
	if ((yield != 0) // yield指定必须切换
        || (count == 0) // 时间片归零
        || (e == NULL) // 当前没有进程
        || (e->env_status != ENV_RUNNABLE) // 当前进程已被阻塞
       ) { // 以上四种条件就需要切换进程
		if (e != NULL && e->env_status == ENV_RUNNABLE) { // 当前有正在运行的进程
			TAILQ_REMOVE(&env_sched_list, e, env_sched_link);
			TAILQ_INSERT_TAIL(&env_sched_list, e, env_sched_link);
		} // 将其插入调度队列的尾部
		panic_on(TAILQ_EMPTY(&env_sched_list));
		e = TAILQ_FIRST(&env_sched_list); // 从队头取一个进程
		count = e->env_pri; // 时间片设置为优先级
		env_run(e);
	} else { // 不切换
		env_run(e);
	}
}

难点分析#

本次lab的最大难点我认为就是进程的地址空间。

首先要认识到每个进程在其认知中,自己都是独占内存空间的。

在此基础上再去理解各个函数。

在此之前,指导书中写道:

虚拟地址 ULIM 是 kseg0 与 kuseg 的分界线,是系统给用户进程分配的最高地址。ULIM 以上的地方,kseg0 和 kseg1 两部分内存的访问不经过 TLB,这部分内存由内核管 理、所有进程共享。在 MOS 操作系统特意将一些内核的数据暴露到用户空间,使得进程不需要切换到内核态就能访问,这是 MOS 特有的设计。

static int env_setup_vm(struct Env *e) { // 这个函数就是来分配进程用户空间的
	struct Page *p;
	try(page_alloc(&p));
	p->pp_ref++;
	e->env_pgdir = (Pde*) page2kva(p);
	// 由于可以直接访问一部分内核数据,因此可以直接拷贝页目录
	memcpy(e->env_pgdir + PDX(UTOP), base_pgdir + PDX(UTOP),
	       sizeof(Pde) * (PDX(UVPT) - PDX(UTOP)));
	// 也可以直接修改页目录
	e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V;
	return 0;
}

还有就是要理解一下中断的处理方式和分发方式,这点在前面写过了,如果没有读懂这段代码extra部分就完蛋了。

其他的部分整体没有lab2难,可能是lab2已经接触过了控制块和队列宏这两个概念,所以lab3在操作的时候会更加熟悉。

实验体会#

本次的课上实验exam部分由于读题不仔细把clocks理解为了进程开始到被调度用的时间间隔,但实际上是进程总共运行的时间总和。导致在课上实验的时候花费了一个多小时来找问题,但大多是无用功,直到后来增加了提示在看懂题目表达的意思是什么。所以在完成实验的时候要好好读题,想明白意思再开始写。而且我还天真的以为只需要写env_stat就能完成,其实要在sched.c里面修改e->scheds

void schedule(int yield) {
	static int count = 0; // remaining time slices of current env
	struct Env *e = curenv;
	count--;
	if ((yield != 0) || (count == 0) || (e == NULL) || (e->env_status != ENV_RUNNABLE)) {
		if (e != NULL && e->env_status == ENV_RUNNABLE) {
			TAILQ_REMOVE(&env_sched_list, e, env_sched_link);
			TAILQ_INSERT_TAIL(&env_sched_list, e, env_sched_link);
		}
		panic_on(TAILQ_EMPTY(&env_sched_list));
		e = TAILQ_FIRST(&env_sched_list);
		count = e->env_pri;
		e->env_scheds++; // 写在这里
		env_run(e);
	} else {
		env_run(e);
	}
}

env_stat反而没有那么难写

void env_stat(struct Env *e, u_int *pri, u_int *scheds, u_int *runs, u_int *clocks) {
	*pri = e->env_pri;
	*scheds = e->env_scheds;
	*runs = e->env_runs;
	if (*runs == 0) {
		e->env_clocks = 0;
	} else {
		e->env_clocks += ((struct Trapframe *)KSTACKTOP - 1)->cp0_count;
	}
	*clocks = e->env_clocks;
}

在extra部分,重点句rs所指向的地址对应的值,应该是*tf->reg[rs]

void do_ri(struct Trapframe *tf) {
    u_long va = tf->cp0_epc;
    Pte *pte;
    page_lookup(curenv->env_pgdir, va, &pte);
    u_long pa = PTE_ADDR(*pte) | (va & 0xfff);
    u_long kva = KADDR(pa);

    int *instr = (int*) kva;
    int opCode = (*instr) >> 26;
    int _rs = ((*instr) >> 21) & 0x1f;
    int _rt = ((*instr) >> 16) & 0x1f;
    int _rd = ((*instr) >> 11) & 0x1f;
    int spc = ((*instr) >> 6) & 0x1f;
    int funcCode = (*instr) & 0x3f;

    if (opCode == 0 && funcCode == 0x3f) { //pmaxud
        u_int rs = tf->regs[_rs];
        u_int rt = tf->regs[_rt];
        u_int rd = 0;
        int i;
        for(i = 0; i < 32; i += 8) {
            u_int rs_i = rs & (0xff << i);
            u_int rt_i = rt & (0xff << i);
            if (rs_i < rt_i) {
                rd = rd | rt_i;
            } else {
                rd = rd | rs_i;
            }
        }
        tf->regs[_rd] = rd;
    } else if (opCode == 0 && funcCode == 0x3e) { //cas
        u_long *rs = tf->regs[_rs];
        u_long tmp = *rs;
        if (*rs == tf->regs[_rt]) {
            *rs = tf->regs[_rd];
        }
        tf->regs[_rd] = tmp;
    }
    tf->cp0_epc += 4;
}
OS Lab3 - 进程与异常
https://alkaid-zhong.github.io/posts/os/os-lab3/
作者
Alkaid
发布于
2024-04-25
许可协议
CC BY-NC-SA 4.0