BUAA_OS_Lab3 实验笔记

文章目录

    • lab3-1
      • PART 1 分配新进程结构体
        • 初始化进程空闲链表
        • 开辟新的进程控制块
      • PART 2 设置进程控制块
        • 给进程分配虚拟空间
        • 进程id相关函数1:进程id的生成 : make env id
        • 进程id相关函数2:根据进程id获得对应进程控制块
      • PART 3 加载二进制镜像
        • 为进程分配栈空间 容纳程序代码
        • 加载elf
      • PART 4 创建一个进程
        • 进程创建
        • 运行进程
    • lab3-2
      • 进程调度
        • 时钟中断的全过程
        • sched_yield 基于时间片的进程调度

lab3-1

PART 1 分配新进程结构体

初始化进程空闲链表

void env_init(void) {
	int i;
	/*Step 1: Initial env_free_list. */
	LIST_INIT(&env_free_list);

	/* Step 2: Travel the elements in 'envs', init every element
	 * (mainly initial its status, mark it as free)
     * and inserts them into the env_free_list as reverse order. */
	for (i = NENV - 1; i >= 0; i--) {
		envs[i].envs_status = ENV_FREE;
		LIST_INSERT_HEAD(&env_free_list, &envs[i], env_link);
	}
}
  • 这是 envs_free_list 初始化的函数。将所有的进程状态置 FREE,代表尚未被使用,并且逆序塞进envs_free_list
  • 逆序了之后实际上在envs_free_list中,。envs[i]是顺序递增的。
  • 逆序的原因:在取用的时候是使用LIST_FIRST宏来取的。
  • envs+i 等价于 &envs[i]
  • 给envs进程数组开了NENV个元素,原因见pmap.c :
    envs = (struct Env *)alloc(NENV * sizeof(struct Env), BY2PG, 1);
  • NENV是2的10次方。所有进程控制块存放的虚拟空间是0x7f40_0000~0x7f80_0000。
  • 除此之外每个进程应该有一个自己的页目录,装的是这个进程对应的页表和物理页之类的。也有一个自己的栈,存放的是程序代码。

开辟新的进程控制块

int env_alloc(struct Env** new, u_int parent_id) /* new: new environment */
{
	int			r;
	struct Env* e;

	/*Step 1: Get a new Env from env_free_list*/
	if (LIST_EMPTY(&env_free_list)) {
		return -E_NO_FREE_ENV;
	}
	e = LIST_FIRST(&env_free_list);

	/* Step 2: Call certain function(has been implemented) to init 
	 * kernel memory layout for this new Env.
     * The function mainly maps the kernel address to this new Env address. */
	if ((r = env_setup_vm(e)) < 0) {
		return r;
	}

	/*Step 3: Initialize every field of new Env with appropriate values*/
	e->env_id = mkenvid(e);
	e->env_status = ENV_RUNNABLE;
	e->env_parent_id = parent_id;

	/*Step 4: focus on initializing env_tf structure, located at this new Env.
     * especially the sp register,CPU status. */
	e->env_tf.cp0_status = 0x10001004;
	e->env_tf.regs[29] = USTACKTOP;

	/*Step 5: Remove the new Env from Env free list*/
	LIST_REMOVE(e, env_link);
	*new = e;
	return 0;
}

本函数(开辟新的进程控制块)步骤:

  1. 从free_list取一个新的进程控制块:如果free_list已经为空,就返回-E_NO_FREE_ENV。否则从LIST_FIRST取一个进程控制块e。
  2. 然后给这个进程控制块分配虚拟空间。env_setup_vm(e)
  3. 初始化进程控制块的一些参数。
  4. 然后正式将这个进程控制块从free_list里移出,并且赋值给new,传递出去。

PART 2 设置进程控制块

给进程分配虚拟空间

static int
env_setup_vm(struct Env* e) {

	int			 i, r;
	struct Page* p = NULL;
	Pde*		 pgdir;

	/* Step 1: Allocate a page for the page directory using a 
	 * function you completed in the lab2.
     * and add its reference.
     * pgdir is the page directory of Env e, assign value for it. */

	if ((r = page_alloc(&p)) < 0) { /* Todo here*/
		panic("env_setup_vm - page alloc error\n");
		return r;
	}
	p->pp_ref++;
	pgdir = (Pde*)page2kva(p);

	/*Step 2: Zero pgdir's field before UTOP. */
	
	for (i = 0; i < PDX(UTOP); i++) {
		pgdir[i] = 0;
	}

	/*Step 3: Copy kernel's boot_pgdir to pgdir. */

	/* Hint:
     *  The VA space of all envs is identical above UTOP
     *  (except at VPT and UVPT, which we've set below).
     *  See ./include/mmu.h for layout.
     *  Can you use boot_pgdir as a template?
     */

	for (i = PDX(UTOP); i <= PDX(~0); i++) {
		pgdir[i] = boot_pgdir[i];
	}

	/*Step 4: Set e->env_pgdir and e->env_cr3 accordingly. */
	e->env_pgdir = pgdir;	  			/* 页目录的虚拟地址。*/
	e->env_cr3 = PADDR(pgdir); 			/* 页目录的物理地址。*/
	// e->env_cr3 = page2pa(p); // is also right

	/*VPT and UVPT map the env's own page table, with
 *      *different permissions. */
	e->env_pgdir[PDX(VPT)] = e->env_cr3;
	e->env_pgdir[PDX(UVPT)] = e->env_cr3 | PTE_V | PTE_R;
	return 0;
}

本函数步骤:

  1. alloc一个页p,作为页目录。
  2. 将页目录的前PDX(UTOP)项清空置零 (Q1:为什么是前PDX(UTOP)项?Q2: 为什么需要置零?)
  3. 将内核页目录拷贝到进程页目录。
    ○ 因为根据./include/mmu.h里面的布局来说,我们其实就是2G/2G模式,用户态占用2G,内核态占用2G。
    ○ 对于所有的进程,他们的页目录在UTOP以上地址的内容(除了UVPT)储存内容应该是相同的 — —
    ○ 上方2G虚拟地址与物理地址对应(只差高位1),这部分由内核管理,对于每个进程来说都一样。所以初始化进程的时候要把上方2G虚存这部分拷贝。因此,在用户进程开启后,访问内核地址不需要切换CR3寄存器。而是可以直接在进程中访问内核地址–》因为我们将内核页目录拷贝到了进程页目录中。
    ○ 然而对于下方2G虚存,下列这段也被映射到内核中。(或许应该称为映射到进程中?但我认为其中一个进程占据了内核那么他就是临时内核)。
        ULIM   	 -----> +----------------------------+------------0x8000 0000-------
 o                      |         User VPT           |     PDMAP                /|\
 o      UVPT     -----> +----------------------------+------------0x7fc0 0000    |
 o                      |         PAGES              |     PDMAP                 |
 o      UPAGES   -----> +----------------------------+------------0x7f80 0000    |
 o                      |         ENVS               |     PDMAP                 |
 o  UTOP,UENVS   -----> +----------------------------+------------0x7f40 0000    |

为什么要将这部分也映射给内核呢?

  • 这部分是什么?
    是ENVS是envs进程数组,PAGES存的是页表结构体,以及不知道到底是什么的User VPT。
  • 这部分什么用?
    ENVS这块,一个4M的用户进程虚拟区,可能是用来给内核一个获得其他进程状态、信息的入口。因此,对于内核来说这部分应该是只读模式。
  1. 将进程页目录的虚拟地址pgdir和物理地址(可以由PADDR宏,或者page2pa宏得到)都赋值给进程结构体e。
  2. 将进程页目录的表示VPTx系统虚拟页目录和UVPT用户虚拟页目录的那4M空间的项都赋值为进程自己的页目录的物理地址,以及加不同的有效位。(Q3: 为什么有效位不同?Q4: 为什么要给VPT和UVPT这样赋值?)

进程id相关函数1:进程id的生成 : make env id

u_int mkenvid(struct Env* e) {
	static u_long next_env_id = 0;

	/*Hint: lower bits of envid hold e's position in the envs array. */
	u_int idx = e - envs;

	/*Hint:  high bits of envid hold an increasing number. */
	/* 生成id */
	return (++next_env_id << (1 + LOG2NENV)) | idx;
}

本函数步骤:

  1. 计算进程索引:第 index 个进程
  2. 生成id:(1 << 11) | index

Thinking:为什么左移11?
我觉得是因为,后面有个函数envid2env(),在计算envid的索引时用的宏ENVX取envid的后十位。也就是说,我觉得可能envid的后十位才表示他的id,如果不左移11位的话,第十位是1,后几位是index,而我们的index不需要前面的1,所以要用左移将它除去。如果没有这个1的话,就无法左移获得一个10位(11位)的数。

进程id相关函数2:根据进程id获得对应进程控制块

int envid2env(u_int envid, struct Env** penv, int checkperm) {
	struct Env* e;
	/* Hint:
 *      *  If envid is zero, return the current environment.*/
	if (envid == 0) {
		*penv = curenv;
		return 0;
	}

	/*Step 1: Assign value to e using envid. */
	// ENVX 取envid的后十位

	e = &envs[ENVX(envid)];

	if (e->env_status == ENV_FREE || e->env_id != envid) {
		*penv = 0;
		return -E_BAD_ENV;
	}
	/* Hint:
 *      *  Check that the calling environment has legitimate permissions
 *      *  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 not, error! */
	/*Step 2: Make a check according to checkperm. */

	if (checkperm && e != curenv && e->env_parent_id != curenv->env_id) {
		*penv = 0;
		return -E_BAD_ENV;
	}

	*penv = e;
	return 0;
}

本函数步骤:

  1. 如果envid是0,返回当前进程控制块。
  2. 获得envid对应的进程索引:ENVX(envid),然后在envs数组中找到对应的元素给e。

Thinking : 为什么要判断e->env_id != envid
因为上一步通过索引取envs数组中的第“id”个进程块e时,去掉了envid的前22位,而只取了后10位。因此,e->env_id != envid这一步确定进程e的id确实是传入的envid。后10位在生成的时候只与进程页的物理位置有关,idx = e - envs。而前面22位才是保证进程unique的关键(由调用次数决定,可以保证unique)。要保证一个进程的id号完全对应,看后十位不够,还得对比前22位也确实是一样的。如果没有这步判断会造成错误:可能输入的id并不是进程id号,而仅仅是进程的物理位置与另一个进程相同。

  1. check一下当前进程curenv是不是有合法perm去操作这个特定进程(要么e是当前进程本身e != curenv,要么e是它的直接子进程e->env_parent_id != curenv->env_id)。
    (Q6: 为什么要check?)

PART 3 加载二进制镜像

为进程分配栈空间 容纳程序代码

static void
load_icode(struct Env* e, u_char* binary, u_int size) {
	/* Hint:
	 *  You must figure out which permissions you'll need
	 *  for the different mappings you create.
	 *  Remember that the binary image is an a.out format image,
	 *  which contains both text and data.
     */
	struct Page* p = NULL;
	u_long		 entry_point;
	u_long		 r;
	u_long		 perm;

	/*Step 1: alloc a page. */

	if(page_alloc(&p) != 0) return -E_NO_MEM;

	/*Step 2: Use appropriate perm to set initial stack for new Env. */
	/*Hint: The user-stack should be writable? */
	// 用第一步申请的页面来初始化一个进程的栈

	if(page_insert(e->env_pgdir,p,USTACKTOP - BY2PG,perm) != 0) return -E_NO_MEM;

	/*Step 3:load the binary by using elf loader. */

	load_elf(binary, size, &entry_point, (void*)e, load_icode_mapper);

	/***Your Question Here***/
	/*Step 4:Set CPU's PC register as appropriate value. */
	// 它指示着进程当前指令所处的位置,
	// 我们要运行的进程的代码段预先被载入到了entry_ point为起点的内存中,
	// 当我们运行进程时,CPU 将自动从pc 所指的位置开始执行二进制码。

	e->env_tf.pc = entry_point;
}

本函数主要步骤:

  1. 申请一个物理页p,用函数page_insert()将物理页p和虚拟地址USTACKTOP - BY2PG联系起来,初始化一个进程的栈,表示常规用户栈normal user stack里一个4KB的一页的空间被使用。
  2. 使用load_elf()函数将每个segment都加载到正确的地方
  3. 将PC寄存器移动到代码入口地址,即entry_point

加载elf

将每个segment都加载到正确的地方

int load_elf(u_char *binary, int size, u_long *entry_point, void *user_data,
			 int (*map)(u_long va, u_int32_t sgsize,
						u_char *bin, u_int32_t bin_size, void *user_data))
{
	Elf32_Ehdr *ehdr = (Elf32_Ehdr *)binary;
	Elf32_Phdr *phdr = NULL;
	 /* As a loader, we just care about segment,
           * so we just parse program headers.
           */
	u_char *ptr_ph_table = NULL;
        Elf32_Half ph_entry_count;
        Elf32_Half ph_entry_size;
        int r;
	
	 // check whether `binary` is a ELF file.
	if (size < 4 || !is_elf_format(binary)) {
                return -1;
    }

    ptr_ph_table = binary + ehdr->e_phoff;
    ph_entry_count = ehdr->e_phnum;
    ph_entry_size = ehdr->e_phentsize;

    while (ph_entry_count--) {
        phdr = (Elf32_Phdr *)ptr_ph_table;

	 /* Your task here!  */
        /* Real map all section at correct virtual address.Return < 0 if error. */
        /* Hint: Call the callback function you have achieved before. */
	// #define PT_LOAD		1		/* Loadable program segment */

        if(phdr->p_type == PT_LOAD) {
        // 打印输出phdr->p_vaddr,发现是UTEXT部分,二进制代码地址是UTEXT
       	// currentE->env_tf.pc = UTEXT + 0xb0;这个在去年的page_alloc里,
       	// 但是今年注释中指出不能把pc设置放在page_alloc里。
            r = map(phdr->p_vaddr, phdr->p_memsz, binary + phdr->p_offset,
                phdr->p_filesz, user_data);
            if(r < 0){
               return r;
            }
        }
        ptr_ph_table += ph_entry_size;
    }

    *entry_point = ehdr->e_entry;
    return 0;
}

本函数主要内容:
本函数的主要功能是在while循环中实现的。主要有两步:

  1. 加载elf文件中的内容到内存。
    ○ 这一步中,先判断这个phdr是不是loadable的。如果可被加载,再加载。
    ○ 然后ptr_ph_table递增一个entry_size的大小。
    ○ phdr指向下一个元素。
  2. 内存富余空间填零。(Q: ???where)
  3. user data

由map函数,即load_icode_mapper()函数实现。void* user_data这个参数是一个函数指针。
函数指针:可以给不同的需要加载的内容动态选择合适的mapper函数。(OSLAB中只有一个mapper函数,其实可以有多个)
mapper函数是把UTEXT的部分映射到新开的page里。


PART 4 创建一个进程

进程创建

void env_create(u_char* binary, int size) {
	/*Step 1: Use env_create_priority to alloc a new env with priority 1 */
	env_create_priority(binary, size, 1);
}

主要是这个函数:

void env_create_priority(u_char* binary, int size, int priority) {
	struct Env* e;
	/*Step 1: Use env_alloc to alloc a new env. */
	// int env_alloc(struct Env** new, u_int parent_id)
	env_alloc(&e, 0);

	/*Step 2: assign priority to the new env. */
	e->env_pri = priority;

	/*Step 3: Use load_icode() to load the named elf binary. */
	load_icode(e, binary, size);
}

本函数主要创建一个进程,步骤:

  1. 分配一个新的Env结构体。
  2. 设置进程控制块,给进程分配虚拟空间。
    以上两步在env_alloc(&e, 0)函数中完成。

Thinking: 这里为什么env_alloc函数的第二个参数是0?
这是一种默认做法。

  1. 将二进制代码载入到对应地址空间。
    load_icode(e, binary, size);完成。

运行进程

void env_run(struct Env* e) {
	/*Step 1: save register state of curenv. */
	// 我们在本实验里的寄存器状态保存的地方是TIMESTACK区域。
	/* Hint: if there is a environment running,you should do
    *  context switch.You can imitate env_destroy() 's behaviors.*/
    // old: 当前进程的上下文所存放的区域

    struct Trapframe *old = (struct Trapframe *)
    						(TIMESTACK - sizeof(struct Trapframe));

    if(curenv != NULL && curenv != e){
    	curenv->env_tf = *old;							// 保存进程上下文
    	curenv->env_tf.pc = curenv->env_tf.cp0_epc;		// 保存当前pc
    }

	/*Step 2: Set 'curenv' to the new environment. */
	
    curenv = e;
    curenv->env_status = ENV_RUNNABLE;

	/*Step 3: Use lcontext() to switch to its address space. */
	
    lcontext(e->env_pgdir);

	/* Step 4: Use env_pop_tf() to restore the environment's
     * environment   registers and drop into user mode in the
     * the   environment.
     */
	/* Hint: You should use GET_ENV_ASID there.Think why? */
	// extern void env_pop_tf(struct Trapframe* tf, int id);
	
	env_pop_tf(&(e->env_tf), GET_ENV_ASID(e->env_id));
}

本函数主要负责进程的切换,步骤:

  1. 保存当前进程的寄存器到 env 的 tf 。设置pc。
    Trapframe : 捕获当前进程的寄存器状态,这个结构体其实就是所有的寄存器。在本实验里的寄存器状态保存的地方是TIMESTACK(时钟栈 0x82000000)区域,所以说指针*old就指向(TIMESTACK - sizeof(struct Trapframe));这意思在栈顶开一个tf大小的空间。然后将上下文保存到当前进程的 curenv->env_tf 中。

Thinking: 关于li sp, 0x82000000
这是在stackframe.h的一句汇编。一个get_sp的宏。我们本次做的都是时钟中断,所以说,存储上下文寄存器的栈指针sp指向的是时钟栈区TIMESTACK。

  1. 恢复要启动的进程。
    ○ 将当前进程curenv设置为新进程e。
    ○ 用lcontext()汇编函数切换地址:将mCONTEXT(页目录首地址)存到a0。并跳转到ra寄存器
    ○ env_pop_tf:把 env 里的 tf 放到寄存器里。
    ○ 把当前进程的id后5位清空。

lab3-2

进程调度

进程调度主要是sched_ yield函数完成的。
调度算法是时间片轮转,在我们的实验中,优先级并不是传统理解中的优先级,而是时间片长度。

  • 在什么时候会调用sched_ yield函数?
  1. 在env_destroy 函数中。这个函数的主要职责就是free一个进程并且调一个进程来运行。但目前为止还没有调用过这个函数。只是声明且定义了它。
  2. 时钟中断。

时钟中断的全过程

  • 什么时候会开启时钟中断?
  1. 进入异常。
 . = 0x80000080;
  .except_vec3 : {
  *(.text.exc_vec3)
  }

首先是进入异常处理程序的入口,一旦CPU发生异常,就自动跳转到0x8000_0080,这里放的是.text.exc_vec3代码。

  1. 选择相应中断处理程序
.section .text.exc_vec3
NESTED(except_vec3, 0, sp)
		.set	noat
		.set	noreorder
		/*
		 * Register saving is delayed as long as we dont know
		 * which registers really need to be saved.
		 */
1:	//j	1b
	nop

		mfc0	k1,CP0_CAUSE
		la	k0,exception_handlers
		/*
		 * Next lines assumes that the used CPU type has max.
		 * 32 different types of exceptions. We might use this
		 * to implement software exceptions in the future.
		 */

		andi	k1,0x7c
		addu	k0,k1
		lw	k0,(k0)
		NOP
		jr	k0
		nop
		END(except_vec3)
		.set	at

关于.set noat 之类
.set是汇编代码的一些设置,比如at,就是开启扩展指令,前面加个no就是不开启。其他命令同理。别的函数里还有一个.set push,是把所有设置存进栈里,相应的还有.set pop

mfc0 k1,CP0_CAUSE 这个汇编函数在设置了之后,首先将CP0_CAUSE给了k1寄存器。
a k0,exception_handlers 然后将异常句柄数组(这个数组的初始化在traps.c里,用set_except_vector这个函数初始化的)的首地址给了k0。
andi k1,0x7c 将k1中的,即CP0的cause寄存器中的异常码区段截出来,就是异常编号。因为c的二进制是1100,也就是在异常码之后还有2个二进制的0,所以相当于将异常编号左移2位,也就是4的整倍数对齐。由于数组以字对齐,也就是异常码+2’b00可以作为异常句柄数组的索引。
addu k0,k1 如上所述,首地址+偏移,得到的是异常码的句柄所在的项的位置。
lw k0,(k0) 汇编语法不太懂,大概就是把找到的异常处理句柄赋值给k0。
jr k0 跳转到对应的异常处理程序。我们在实验中暂时只实现了handle_int这个句柄。

  1. 保存现场
NESTED(handle_int, TF_SIZE, sp)
.set	noat

//1: j 1b
nop

SAVE_ALL			// 保存栈帧,把所有的寄存器给保存到栈中
CLI
.set	at
mfc0	t0, CP0_CAUSE
mfc0	t2, CP0_STATUS
and	t0, t2

andi	t1, t0, STATUSF_IP4
bnez	t1, timer_irq				// 判断是否支持中断。如果支持中断,则调用timer_irq

nop
END(handle_int)

本段汇编函数:
分别将CP0_CAUSE和CP0_STATUS存入t0和t2两个寄存器中,然后and,存入t0,然后就可以获得具体中断号(ppt里看的,并不知道具体怎么操作的)。
然后判断是否支持中断。如果支持中断,则调用timer_irq。

  1. 调用timer_irq
    函数里只有一句跳转到sched_yield和返回(ret_from_exception)

sched_yield 基于时间片的进程调度

void sched_yield(void)
{
    // 记录当前进程已经使用的时间片数目
    static int count = 0;
    // t 记录进程链表序号,0或1
    static int t = 0;
    // 当前进程已使用时间片+1
    count++;
    /*
     * 切换进程的条件
     * 1. 当前进程时NULL,这种情况只发生在运行第一个进程的时候。
     * 此时还没env_run(),而这个函数负责将curenv设置为e。
     * 2. 当前进程的时间片已经用完了
    */
    if(curenv == NULL || count >= curenv->env_pri) {
        // 如果不是第一次运行进程,则要将当前进程添加到另一个待调度队列中以便下一次调度。
        // 为什么是insert_tail呢,我觉得和链表每个进程能被公平调度有关。
       	// 取用的时候是list_first,放回的时候就塞到队尾。
        if(curenv != NULL) {
            LIST_INSERT_HEAD(&env_sched_list[1 - t], curenv, env_sched_link);
        }
        // 若两个链表都没找到合适进程,就返回。这个flag用于标志已经遍历过的链表。
        int flag = 0; 
        while(1) {
            struct Env *e = LIST_FIRST(&env_sched_list[t]);
            // 若当前进程列表没有可以调度的进程,就换一个链表。
            // 同时flag+1,表示已经遍历了其中一个链表。
            if(e == NULL) {
                if(flag == 0) flag = 1;
                else return; // 若两个链表都没有找到,就直接return返回。
                t = 1 - t; // 换一个链表找,这里用t^=1也可,并且位运算速度比加减法快。
                continue;
            }
            // 若找到一个可以执行的进程
            if(e->env_status == ENV_RUNNABLE){
                // 从待调度队列中移出
                LIST_REMOVE(e,env_sched_link);
                // 初始化已使用的时间片个数
                count = 0;
                // 运行找到的这个进程e,当成当前进程
                env_run(e);
                break;
            }
        }
    } else {
    // 如果剩余时间片不为0,将剩余时间片-1,并且env_run接着执行当前进程。
        env_run(curenv);
    }
}

在网上有好多种写法,但主要意思都是一样的。
进程调度的主要思路是:进入这个函数 -》当前进程已用时间片+1 -》若当前进程用完时间片-》找一个新的进程并运行-》若当前进程没有用完时间片-》继续运行当前进程。
详细来说见注释。
此外,

  • 双队列减少进程间的不公平。
  • 两个队列都存储RUNNABLE的进程,减少遍历时间。
  • 需要在priority设置完之后再把env插入到第一个队列中。而不是设置完status之后插入,因为此时priority默认为0,也就是时间片还是0就被加入待调用的进程链表中。(对ppt抱有异议)

你可能感兴趣的:(BUAA,OS)