Try to be kind and useful.

BUAA-OS-lab3

lab3

1.创建一个进程并成功运行

2.实现时钟中断,通过时钟中断内核可以再次获得执行权

3.实现进程调度,创建两个进程,并且通过时钟中断切换进程执行

在本次实验中你将运行一个用户模式的进程。

你需要使用数据结构进程控制块 Env 来跟踪用户进程,并建立一个简单的用户进程,加载一个程序镜像到指定的内存空间,然后让它运行起来。

同时,你的MIPS 内核将拥有处理异常的能力。

hint:为了更加深刻理解中断异常处理,建议在做本实验的中断异常部分之前阅读《See MIPS Run Linux》的第三章及第五章,复习计组中学过的MIPS中断异常机制,尤其要深刻理解CP0寄存器的工作原理。

1.进程控制块

进程既是基本的分配单元,也是基本的执行单元。每个进程都是一个实体,有其自己的地址空间,通常包括代码段、数据段和堆栈。程序是一个没有生命的实体,只有被处理器赋予生命时,它才能成为一个活动的实体,而执行中的程序,就是我们所说的进程。

进程控制块 (PCB) 是系统专门设置用来管理进程的数据结构,它可以记录进程的外部特征,描述进程的运动变化过程。系统利用 PCB 来控制和管理进程,所以 PCB是系统感知进程存在的唯一标志。进程与 PCB 是一一对应的。通常 PCB 应包含如下一些信息:

struct Env {
    struct Trapframe env_tf;       // Saved registers
    LIST_ENTRY(Env) env_link;      // Free LIST_ENTRY
    u_int env_id;                  // Unique environment identifier
    u_int env_parent_id;           // env_id of this env's parent
    u_int env_status;              // Status of the environment
    Pde *env_pgdir;                // Kernel virtual address of page dir
    u_int env_cr3;
    LIST_ENTRY(Env) env_sched_link;
    u_int env_pri;
};
  • env_tf:定义在include/trap.h中,在进程切换时,会将当前进程的上下文环境保存在env_tf变量中:

struct Trapframe { //lr:need to be modified(reference to linux pt_regs) TODO
   /* Saved main processor registers. */
   unsigned long regs[32];

   /* Saved special registers. */
   unsigned long cp0_status;
   unsigned long hi;
   unsigned long lo;
   unsigned long cp0_badvaddr;
   unsigned long cp0_cause;
   unsigned long cp0_epc;
   unsigned long pc;
};
  • env_link:机制类似于pp_link,使用此来搭配env_free_list来构造空闲链表

#define LIST_ENTRY(type)                                           
       struct {                                                    
               struct type *le_next;   /* next element */            
               struct type **le_prev;  /* address of previous next element *  
       }
#define LIST_NEXT(elm, field) ((elm)->field.le_next)
  • env_id:进程独一无二的标识符

  • env_parent_id:进程可以被其他进程创建,记录父进程的id

  • env_status:

    • ENV_FREE:表明该进程处于进程空闲链表中

    • ENV_NOT_RUNNABLE:进程处于阻塞状态,处于阻塞状态的进程需要在一定条件下变成就绪状态从而被CPU调度

    • ENV_RUNNABLE:进程处于执行或就绪状态,也可能正在运行,也可能正在等待被调度。

 

注意到就绪状态不会直接进入阻塞状态,阻塞状态不会直接进入执行状态

  • env_pgdir:保存了该进程页目录的内核虚拟地址

  • env_cr3:保存了该进程页目录的物理地址

  • env_sched_link:用于构造调度队列

  • env_pri:这个变量保存了进程的优先级

 

(课程理论图片)

Exercise 3.1

阅读mips_vm_init函数

Exercise 3.2

Overview:

  • Mark all environments in 'envs' as free and insert them into the env_free_list.

  • Insert in reverse order,so that the first call to env_alloc() returns envs[0].

  • Hints:

  • You may use these macro definitions below:

  • LIST_INIT, LIST_INSERT_HEAD

void
env_init(void)
{
   int i;
   /* Step 1: Initialize env_free_list. */
LIST_INIT(&env_free_list);
   LIST_INIT(env_sched_list);
   LIST_INIT(env_sched_list + 1);
   
   for (i = NENV-1; i >= 0; i--) {
       envs[i].env_status = ENV_FREE;
       LIST_INSERT_HEAD(&env_free_list, envs + i, env_link);
  }

   /* Step 2: Traverse the elements of 'envs' array,
    *   set their status as free and insert them into the env_free_list.
    * Choose the correct loop order to finish the insertion.
    * Make sure, after the insertion, the order of envs in the list
    *   should be the same as that in the envs array. */


}

2.进程的标识

mkenvid:作用就是生成一个新的进程id

u_int mkenvid(struct Env *e) {
   u_int idx = e - envs;
   u_int asid = asid_alloc();//从0到63中选一个作为asid
   return (asid << (1 + LOG2NENV)) | (1 << LOG2NENV) | idx;
}

asid_alloc函数:作用是为新创建的进程分配一个异于当前所有未被释放的进程的 ASID。

为什么不采用单纯的自增?可以发现,其中 ASID 部分只占据了 6-11 共 6 个 bit,所以如果单纯的通过自增的方式来分配 ASID 的话,很快就会发生溢出,导致 ASID 重复。

为了解决这个问题,我们采用限制同时运行的进程个数的方法来防止 ASID 重复。具体实现是通过位图法管理可用的 64 个 ASID,如果当 ASID 耗尽时仍要创建进程,系统会 panic。

static u_int asid_alloc() {
   int i, index, inner;
   for (i = 0; i < 64; ++i) {
       index = i >> 5;//相当于除32
       inner = i & 31;//在32以内的偏移量
       //在32范围内,每一个进程对应于位图数组中整数的某一位
       if ((asid_bitmap[index] & (1 << inner)) == 0) {
           //表示该位对应的进程id未被使用
           asid_bitmap[index] |= 1 << inner;
           return i;
      }
  }
   panic("too many processes!");
}

Exercise 3.3

/* Overview:
* Convert an envid to an env pointer.
* If envid is 0 , set *penv = curenv; otherwise set *penv = envs[ENVX(envid)];
*
* Pre-Condition:
* penv points to a valid struct Env * pointer,
* envid is valid, i.e. for the result env which has this envid,
* its status isn't ENV_FREE,
* checkperm is 0 or 1.
*
* Post-Condition:
* return 0 on success,and set *penv to the environment.
* return -E_BAD_ENV on error,and set *penv to NULL.
*/
int envid2env(u_int envid, struct Env **penv, int checkperm)
{
struct Env *e;
/* Hint: If envid is zero, return curenv.*/
/* Step 1: Assign value to e using envid. */
if (envid == 0) {
*penv = curenv;
return 0;
} else {
e = &envs[ENVX(envid)];
}

if (e->env_status == ENV_FREE || e->env_id != envid) {
*penv = NULL;
return -E_BAD_ENV;
}
/* Hints:
* Check whether the calling env has sufficient permissions
* to manipulate the specified env.
* If checkperm is set, the specified env
* must be either curenv or an immediate child of curenv.
* If not, error! */
/* Step 2: Make a check according to checkperm. */
if (checkperm) {
if (e != curenv && e->env_parent_id != curenv->env_id) {
*penv = NULL;
return -E_BAD_ENV;
}
}

*penv = e;
return 0;
}

Thinking 3.1

答:通过e = &envs[ENVX(envid)]只能保证获取的进程块的env_id的低十位与envid相同,但是得到的进程块可能已经被替换,即 asid发生了变化,因而必须对比完整的 env_id才能确定得到正确的进程块。

3.设置进程控制块

进程创建流程:

第一步 申请一个空闲的PCB(也就是Env结构体),从env_free_list 中索取一个空闲PCB 块,这时候的PCB 就像张白纸一样。

第二步 “纯手工打造”打造一个进程。在这种创建方式下,由于没有模板进程, 所以进程拥有的所有信息都是手工设置的。而进程的信息又都存放于进程控制块中,所以我们需要手工初始化进程控制块。

第三步 进程光有PCB 的信息还没法跑起来,每个进程都有独立的地址空间。所以,我们要为新进程分配资源,为新进程的程序和数据以及用户栈分配必要的内存空间。

第四步 此时PCB 已经被填写了很多东西,不再是一张白纸,把它从空闲链表里摘出, 就可以投入使用了。

env.c 中的env_setup_vm 函数就是你在第二步中要使用的函数,该函数的作用是初始化新进程的地址空间,也即是初始化该进程的页目录

Exercise 3.4

/* Overview:
* Initialize the kernel virtual memory layout for 'e'.
* Allocate a page directory, set e->env_pgdir and e->env_cr3 accordingly,
* and initialize the kernel portion of the new env's address space.
* DO NOT map anything into the user portion of the env's virtual address space.
*/
/*** exercise 3.4 ***/
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 pp_ref.
* pgdir is the page directory of Env e, assign value for it. */
if (r = page_alloc(&p)) {
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. */
for ( ; i < 1024; i++) {
if (i != PDX(UVPT)) {
pgdir[i] = boot_pgdir[i];
}
}
/* Hint:
* The VA space of all envs is identical above UTOP
* (except at UVPT, which we've set below).
* See ./include/mmu.h for layout.
* Can you use boot_pgdir as a template?
*/
e->env_pgdir = pgdir;
e->env_cr3 = PADDR(pgdir);
/* UVPT maps the env's own page table, with read-only permission. */
e->env_pgdir[PDX(UVPT)] = e->env_cr3 | PTE_V;
return 0;
}

Thinking 3.2

1.UTOP和ULIM的含义:UTOP代表地址0x7f40 0000 ,是用户进程读写的最高地址,而ULIM代表地址0x8000 0000,是用户进程的最高地址。UTOP到ULIM之间的区域有ENVS、PAGES、User VPT,对应用户进程的进程块及页表等,用户只有读的权限。

2.pdgir[PDX(UVPT)]对应页目录的页目录项,存储的是页目录的物理地址(env_cr3

3.每个进程都有自己独立的虚拟地址空间,不同的进程的同一虚拟地址有可能映射到不同的物理地址,但是进程的切换保证了进程之间的虚拟地址不会相互影响,而是总能在页表中找到对应于自身的正确的物理地址。

Exercise 3.5

/* Overview:
* Allocate and Initialize a new environment.
* On success, the new environment is stored in *new.
*
* Pre-Condition:
* If the new Env doesn't have parent, parent_id should be zero.
* env_init has been called before this function.
*
* Post-Condition:
* return 0 on success, and set appropriate values of the new Env.
* return -E_NO_FREE_ENV on error, if no free env.
*
* Hints:
* You may use these functions and macro definitions:
* LIST_FIRST,LIST_REMOVE, mkenvid (Not All)
* You should set some states of Env:
* id , status , the sp register, CPU status , parent_id
* (the value of PC should NOT be set in env_alloc)
*/
/*** exercise 3.5 ***/
int
env_alloc(struct Env **new, u_int parent_id)
{
int r;
struct Env *e;

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

/* Step 2: Call a certain function (has been completed just now) to init kernel memory layout for this new Env.
*The function mainly maps the kernel address to this new Env address. */
r = env_setup_vm(e);
if (r) {
*new = NULL;
return -E_NO_FREE_ENV;
}
/* 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 the sp register and cp0_status of env_tf field, located at this new Env. */
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;
}

 

第28bit 设置为1,表示允许在用户模式下使用 CP0 寄存器。

第12bit 设置为1,表示4 号中断可以被响应。

R3000 的 SR 寄存器的低六位是一个二重栈的结构。KUo 和 IEo 是一组,每当中断发生的时候,硬件自动会将 KUp 和 IEp 的数值拷贝到这里;KUp 和 IEp 是一组,当中断发生的时候,硬件会把 KUc 和 IEc 的数值拷贝到这里。

其中KU 表示是否位于内核模式下,为1 表示位于内核模式下;IE 表示中断是否开启,为1 表示开启,否则不开启。

而每当rfe 指令调用的时候,就会进行上面操作的逆操作

最后六位设置为000100的原因:

当运行进程前,运行上述代码到rfe的时候 (rfe 处于延迟槽中),就会将 KUp 和 IEp 拷贝回 KUc 和 IEc,令status 为 000001,最后两位 KUc,IEc 为 [0,1],表示开启了中断。之后第一个进程成功运行,这时操作系统也可以正常响应中断

4.加载二进制镜像

我们需要为新进程的程序分配空间来容纳程序代码。

MemSiz(即sgsize) 永远大于等于 FileSiz(即bin_size)。若 MemSiz 大于 FileSiz,则操作系统在加载程序的时候,会首先将文件中记录的数据加载到对应的 VirtAddr 处。之后,向内存中填 0, 直到该段在内存中的大小达到 MemSiz 为止。那么为什么 MemSiz有时候会大于 FileSiz 呢?这里举这样一个例子:C 语言中未初始化的全局变量,我们需要为其分配内存,但它又不需要被初始化成特定数据。因此,在可执行文件中也只记录它需要占用内存 (MemSiz),但在文件中却没有相应的数据(因为它并不需要初始化成特定数据)。故而在这种情况下,MemSiz 会大于 FileSiz。这也解释了,为什么 C 语言中全局变量会有默认值 0。这是因为操作系统在加载时将所有未初始化的全局变量所占的内存统一填了 0。

/* Overview:
* This is a call back function for kernel's elf loader.
* Elf loader extracts each segment of the given binary image.
* Then the loader calls this function to map each segment
* at correct virtual address.
*
* `bin_size` is the size of `bin`. `sgsize` is the
* segment size in memory.
*
* Pre-Condition:
* bin can't be NULL.
* Hint: va may be NOT aligned with 4KB.
*
* Post-Condition:
* return 0 on success, otherwise < 0.
*/
/*** exercise 3.6 ***/
static int load_icode_mapper(u_long va, u_int32_t sgsize,
u_char *bin, u_int32_t bin_size, void *user_data)
{
struct Env *env = (struct Env *)user_data;
struct Page *p = NULL;
u_long i = 0;//当前已处理的空间大小
int r;
u_long offset = va - ROUNDDOWN(va, BY2PG);
long size = 0;

if (bin == NULL) return -1;

u_long perm = PTE_R;

//处理第一个页面可能存在的未对齐
if (offset) {
size = MIN(bin_size, (BY2PG - offset));
p = page_lookup(env->env_pgdir, va + i, NULL);
if (p == 0) {
if (r = page_alloc(&p)) {
return r;
}
page_insert(env->env_pgdir, p, va + i, perm);
}
bcopy((void *)bin, (void *)(page2kva(p) + offset), size);
i += size;
}

//处理bin_size以下的复制
for ( ; i < bin_size; i += size) {
/* Hint: You should alloc a page.*/
size = MIN(bin_size - i, BY2PG);
p = page_lookup(env->env_pgdir, va + i, NULL);
if (p == 0) {
if (r = page_alloc(&p)) {
return r;
}
page_insert(env->env_pgdir, p, va + i, perm);
}
bcopy((void *)(bin + i), (void *)(page2kva(p)), size);
}

//处理bin_size以上的一页大小范围内的未对齐的填零
offset = va + i - ROUNDDOWN((va + i), BY2PG);
if (offset) {
size = MIN(BY2PG - offset, sgsize - i);
p = page_lookup(env->env_pgdir, va + i, NULL);
if (p == 0) {
if (r = page_alloc(&p)) {
return r;
}
page_insert(env->env_pgdir, p, va + i, perm);
bcopy((void *)(bin + i - offset), (void *)(page2kva(p)), offset);
}
bzero((void *)(page2kva(p) + offset), size);
i += size;
}

//处理sgsize的填零
while (i < sgsize) {
size = MIN(BY2PG, sgsize - i);
p = page_lookup(env->env_pgdir, va + i, NULL);
if (p == 0) {
if (r = page_alloc(&p)) {
return r;
}
page_insert(env->env_pgdir, p, va + i, perm);
}
bzero((void *)page2kva(p), size);
i += size;
}

return 0;
}
static int load_icode_mapper(u_long va, u_int32_t sgsize,
u_char *bin, u_int32_t bin_size, void *user_data)
{
struct Env *env = (struct Env *)user_data;
struct Page *p = NULL;
u_long i = 0;
int r;
u_long offset = va - ROUNDDOWN(va, BY2PG);
long size = 0;

if (bin == NULL) return -1;

u_long perm = PTE_R;

if (offset) {
p = page_lookup(env->env_pgdir, va + i, NULL);
if (p == 0) {
if (r = page_alloc(&p)) {
return r;
}
if (r = page_insert(env->env_pgdir, p, va + i, perm)) {
return r;
}
}
size = MIN(bin_size, BY2PG - offset);
bcopy((void *)bin, (void *)(page2kva(p) + offset), size);
}
for (i = size; i < bin_size; i += BY2PG) {

if (r = page_alloc(&p)) {
return r;
}
if (r = page_insert(env->env_pgdir, p, va + i, perm)) {
return r;
}
size = MIN(bin_size - i, BY2PG);
bcopy((void *)(bin + i), (void *)(page2kva(p)), size);
}
while (i < sgsize) {
if (r = page_alloc(&p)) {
return r;
}
if (r = page_insert(env->env_pgdir, p, va + i, perm)) {
return r;
}
i += BY2PG;
}
return 0;
}

“自定义函数”的框架:load_elf() 函数会从ELF 文件文件中解析出每个segment 的四个信息:va(该段需要被加载到的虚地址)、sgsize(该段在内存中的大小)、bin(该段在ELF 文件中的起始位置)、bin_size(该段在文件中的大小),并将这些信息传给我们的“自定义函数”。

Thinking 3.3

找到 user_data 这一参数的来源,思考它的作用。没有这个参数可不可以?为什么?(可以尝试说明实际的应用场景,举一个实际的库中的例子)

答:user_data的数据类型为让用户自由选择传入的参数类型,

在函数load_icode_mapper中,被传入的user_data被用于这样一个语句中:

struct Env *env = (struct Env *)user_data;

因此这个所谓的user_data实际上在函数中的真正含义就是这个被操作的进程指针。在load_elf函数中,我们可以看到user_data从函数本身被传入到调用load_icode_mapper中没有改变,那再回溯到调用load_elfload_icode中,我们发现在调用load_elf时的语句为:

r = load_elf(binary, size, &entry_point, e, load_icode_mapper);

而其中的e则为传入load_icode中的struct Env *e,因此我们的推测得到证实。

如果没有进程指针,我们的加载镜像的步骤显然不能正常完成。

Exercise 3.6 (实验难点)

第一步 加载该段在ELF 文件中的所有内容到内存。

第二步 如果该段在文件中的内容的大小达不到为填入这段内容新分配的页面大小,即 alloc 了新的页表但没能填满,那么余下的部分用0 来填充。

下图展示的是“最糟糕”的情况:

 

Thinking 3.4

结合load_icode_mapper 的参数以及二进制镜像的大小,考虑该函数可能会面临哪几种复制的情况?你是否都考虑到了?

答:

1.va起始虚拟地址可能未对齐,此时为了信息安全,应该先寻找该页面对应的page结构体,若没有则创建。将bin起始的size = MIN(bin_size, (BY2PG - offset))字节的内容复制到页面起始偏移offset处。其中, offset = va - ROUNDDOWN(va, BY2PG)

2.接下来处理binsize以下的内容复制:每次复制 size = MIN(bin_size - i, BY2PG)大小的内容到内存中;

3.完成以上处理后,可能仍有未对齐,我们需要处理bin_size以上的一页大小范围内的未对齐的填零,size = MIN(BY2PG - offset, sgsize - i),其中 offset = va + i - ROUNDDOWN((va + i), BY2PG)

4.处理sgsize以下的填零, size = MIN(BY2PG, sgsize - i)

Exercise 3.7

/* Overview:
* Sets up the the initial stack and program binary for a user process.
* This function loads the complete binary image by using elf loader,
* into the environment's user memory. The entry point of the binary image
* is given by the elf loader. And this function maps one page for the
* program's initial stack at virtual address USTACKTOP - BY2PG.
*
* Hints:
* All mapping permissions are read/write including text segment.
* You may use these :
* page_alloc, page_insert, page2kva , e->env_pgdir and load_elf.
*/
/*** exercise 3.7 ***/
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. */
​ r = page_alloc(&p);
if (r) return;
/* Step 2: Use appropriate perm to set initial stack for new Env. */
perm = PTE_R;
r = page_insert(e->env_pgdir, p, USTACKTOP - BY2PG, perm);
//分配进程的运行栈空间,注意这里是用户栈。
if (r) return;
/*Hint: Should the user-stack be writable? */

/* Step 3:load the binary using elf loader. */
​ r = load_elf(binary, size, &entry_point, (void *)e, load_icode_mapper);
if (r) return;
/* Step 4:Set CPU's PC register as appropriate value. */
e->env_tf.pc = entry_point;
}
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;

if (phdr->p_type == PT_LOAD) {
/* Your task here! */
/* Real map all section at correct virtual address.Return < 0 if error. */
/* Hint: Call the callback function you have achieved before. */
if (r = map(phdr->p_vaddr, phdr->p_memsz, binary + phdr->p_offset, phdr->p_filesz, user_data)) {
return r;
}
}

ptr_ph_table += ph_entry_size;
}

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

 

Thinking 3.5

答:1.env_tf.pc中存储的是虚拟地址

  1. entry_point对于每个进程并不相同,因为每个进程的起始地址都可能不相同。这种差异使得进程之间更加独立。

5.创建进程

Exercise 3.8

/* Overview:
* Allocate a new env with env_alloc, load the named elf binary into
* it with load_icode and then set its priority value. This function is
* ONLY called during kernel initialization, before running the FIRST
* user_mode environment.
*
* Hints:
* this function wraps the env_alloc and load_icode function.
*/
/*** exercise 3.8 ***/
void
env_create_priority(u_char *binary, int size, int priority)
{
struct Env *e;
/* Step 1: Use env_alloc to alloc a new env. */
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,
and insert it into env_sched_list using LIST_INSERT_HEAD. */
load_icode(e, binary, size);
LIST_INSERT_HEAD(env_sched_list, e, env_sched_link);
}
/* Overview:
* Allocate a new env with default priority value.
*
* Hints:
* this function calls the env_create_priority function.
*/
/*** exercise 3.8 ***/
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);

}

6.进程运行与切换

6.1 env_run,是进程运行使用的基本函数,它包括两部分:

• 保存当前进程上下文(如果当前没有运行的进程就跳过这一步)

• 恢复要启动的进程的上下文,然后运行该进程。

6.2 两种需要保存的信息:

进程本身的信息

进程周围的环境信息

事实上,进程本身的信息无非就是进程控制块中那些字段,包括

env_id,env_parent_id,env_pgdir,env_cr3...

这些在进程切换后还保留在原本的进程控制块中,并不会改变,因此不需要保存。而会变的实际上是进程周围的环境信息,这才是需要保存的内容。也就是 env_tf 中的进程上下文。

Thinking 3.6

请查阅相关资料解释,上面提到的epc是什么?为什么要将env_tf.pc设置为epc呢?

答:epc是cp0中的寄存器,用于保存异常发生时指令跳转前的执行位置,将env_tf.pc设置为epc可以保证下次进程收到调度继续执行时可以从上次中断的位置继续。

env_run 的执行流程:

  1. 保存当前进程的上下文信息,设置当前进程上下文中的 pc 为epc。

  2. 切换 curenv 为即将运行的进程。

  3. 调用 lcontext 函数,设置全局变量mCONTEXT为当前进程页目录地址,这个值将在TLB重填时用到。

  4. 调用 env_pop_tf 函数(定义在 lib/env_asm.S 中的一个汇编函数),恢复现场、异常返回。

Thinking 3.7

关于 TIMESTACK,请思考以下问题:

操作系统在何时将什么内容存到了 TIMESTACK 区域

TIMESTACK 和 env_asm.S 中所定义的 KERNEL_SP 的含义有何不同

答:1.操作系统在进程切换时将当前进程存储上下文环境的env_tf结构体存在了TIMESTACK区域。

2.

.macro get_sp
mfc0 k1, CP0_CAUSE
andi k1, 0x107C
xori k1, 0x1000
bnez k1, 1f
nop
li sp, 0x82000000
j 2f
nop
1:
bltz sp, 2f
nop
lw sp, KERNEL_SP
nop

2: nop


.endm

从上面那段汇编代码中我们可以看出,将栈指针设在TIMESTACK还是KERNEL_SPCP0_CAUSE有关,经查阅,在发生中断时将进程的状态保存到TIMESTACK中,在发生系统调用时,将进程的状态保存到KERNEL_SP中。

Exercise 3.10

extern void env_pop_tf(struct Trapframe *tf, int id);
extern void lcontext(u_int contxt);

/* Overview:
* Restore the register values in the Trapframe with env_pop_tf,
* and switch the context from 'curenv' to 'e'.
*
* Post-Condition:
* Set 'e' as the curenv running environment.
*
* Hints:
* You may use these functions:
* env_pop_tf , lcontext.
*/
/*** exercise 3.10 ***/
void
env_run(struct Env *e)
{
/* Step 1: save register state of curenv. */
/* Hint: if there is a environment running,
* you should switch the context and save the registers.
* You can imitate env_destroy() 's behaviors.*/
struct Trapframe *old;
old = (struct Trapframe *)(TIMESTACK - sizeof(struct Trapframe));
if (curenv != NULL) {//big bug
bcopy((void*)old, (void*)(&(curenv->env_tf)), sizeof(struct Trapframe));
curenv -> env_tf.pc = curenv -> env_tf.cp0_epc;
}
/* Step 2: Set 'curenv' to the new environment. */
curenv = e;
/* Step 3: Use lcontext() to switch to its address space. */
lcontext((int)e->env_pgidr);
/* Step 4: Use env_pop_tf() to restore the environment's
* environment registers and return to user mode.
*
* Hint: You should use GET_ENV_ASID there. Think why?
* (read <see mips run linux>, page 135-144)
*/
env_pop_tf(&(e->env_tf), GET_ENV_ASID(e->env_id));
}

env_destroy ,其实就是把 old 区域的东西拷贝到当前进程的 env_tf 中,以达到保存进程上下文的效果。

void
env_destroy(struct Env *e)
{
/* Hint: free e. */
env_free(e);

/* Hint: schedule to run a new environment. */
if (curenv == e) {
curenv = NULL;
/* Hint: Why this? */
bcopy((void *)KERNEL_SP - sizeof(struct Trapframe),
(void *)TIMESTACK - sizeof(struct Trapframe),
sizeof(struct Trapframe));
printf("i am killed ... \n");
sched_yield();
}
}
extern void env_pop_tf(struct Trapframe *tf, int id);
extern void lcontext(u_int contxt);

Exercise 3.11

struct Env {
struct Trapframe env_tf; // Saved registers
LIST_ENTRY(Env) env_link; // Free LIST_ENTRY
u_int env_id; // Unique environment identifier
u_int env_parent_id; // env_id of this env's parent
u_int env_status; // Status of the environment
Pde *env_pgdir; // Kernel virtual address of page dir
u_int env_cr3;
LIST_ENTRY(Env) env_sched_link;
u_int env_pri;
};

在进程切换时,我们会将寄存器的值保存在 TIMESTACK 对应的页面。但是在 lab2 初始化页表时,这一页也被设置为了“空闲”状态,所以此物理页面可能会被进程占用导致问题。现在需要同学们修改 page_init 函数来保证这种问题不会发生。

#define TIMESTACK 0x82000000
for (p = pa2page(PADDR(freemem)); page2ppn(p) < npage; p++) {
if (p == pa2page(PADDR(TIMRSTACK) - 1)) {
p->pp_ref = 1;
} else {
p->pp_ref = 0;
LIST_INSERT_HEAD(&page_free_list, p, pp_link);
}
}
 

异常处理:

寄存器助记符CP0寄存器编号描述
SR 12 状态寄存器,包括中断引脚使能,其他 CPU 模式等位域
Cause 13 记录导致异常的原因
EPC 14 异常结束后程序恢复执行的位置

SR 寄存器:下图是 MIPS R3000 中 Status Register寄存器,15-8 位为中断屏蔽位,每一位代表一个不同的中断活动,其中 15-10 位使能外部中断源,9-8 位是 Cause 寄存器中可写的中断位

 

Cause 寄存器:下图是 MIPS R3000 中 Cause 寄存器。其中保存着 CPU 中哪一些中断或者异常已经发生。15-8 位保存着哪一些中断发生了,其中 15-10 位来自硬件,9-8 位可以由软件写入,当 SR 寄存器中相同位允许中断(为 1)时,Cause 寄存器这一位活动就会导致中断。6-2 位(ExcCode),记录发生了什么异常。

 

异常的分发:

Exercise 3.12

.section .text.exc_vec3
NESTED(except_vec3, 0, sp)
   .set noat
   .set noreorder
1:
   mfc0 k1,CP0_CAUSE
   la k0,exception_handlers
   andi k1,0x7c  #1111100,取2-6位
   addu k0,k1
   lw k0,(k0)
   nop
   jr k0
   nop
END(except_vec3)
.set at
  1. 将 CP0_CAUSE 寄存器的内容拷贝到 k1 寄存器中。

  2. 将 execption_handlers 基地址拷贝到 k0。

  3. 取得 CP0_CAUSE 中的 2~6 位,也就是对应的异常码,这是区别不同异常的重要标志。

  4. 以得到的异常码作为索引去 exception_handlers 数组中找到对应的中断处理函数,后文中会有涉及。

  5. 跳转到对应的中断处理函数中,从而响应了异常,并将异常交给了对应的异常处理函数去处理。

Exercise 3.13

.text.exc_vec3 段需要被链接器放到特定的位置,在 R3000 中这一段是要求放到地址 0x80000080 处,这个地址处存放的是异常处理程序的入口地址。一旦 CPU 发生异常,就会自动跳转到地址 0x80000080 处,开始执行。

. = 0x80000080;
.except_vec3 : {
  *(.text.exc_vec3)
}

异常向量组:

exception_handlers:libs/traps.c

extern void handle_int();
extern void handle_reserved();
extern void handle_tlb();
extern void handle_sys();
extern void handle_mod();
unsigned long exception_handlers[32];

void trap_init()
{
   int i;
   for (i = 0; i < 32; i++) {
       set_except_vector(i, handle_reserved);
  }

   set_except_vector(0, handle_int);
   set_except_vector(1, handle_mod);
   set_except_vector(2, handle_tlb);
   set_except_vector(3, handle_tlb);
   set_except_vector(8, handle_sys);
}

void *set_except_vector(int n, void *addr)
{
   unsigned long handler = (unsigned long)addr;
   unsigned long old_handler = exception_handlers[n];
   exception_handlers[n] = handler;
   return (void *)old_handler;
}

0 号异常的处理函数为handle_int,表示中断,由时钟中断、控制台中断等中断造成

1 号异常的处理函数为handle_mod,表示存储异常,进行存储操作时该页被标记为只读

2 号异常的处理函数为handle_tlb,TLB 异常,TLB 中没有和程序地址匹配的有效入口

3 号异常的处理函数为handle_tlb,TLB 异常,TLB 失效,且未处于异常模式(用于提高处理效率)

8 号异常的处理函数为handle_sys,系统调用,陷入内核,执行了 syscall 指令

Thinking 3.8

handle_int:lib/genex.S

handle_mod:lib/genex.S

handle_tlb:lib/genex.S

handle_tlb:lib/genex.S

handle_sys:lib/syscall.S

在我们的实验中,我们主要是使用 0 号异常,即中断异常的处理函数。而我们接下来要做的,就是产生时钟中断。

时钟中断:

从 CPU 到操作系统中关于中断处理的普遍性流程:

  1. 将当前 PC 地址存入 CP0 中的 EPC 寄存器。

  2. 将 IEc,KUc 拷贝至 KUp 和IEp 中,同时将 IEc 置为 0,表示关闭全局中断使能,将 KUc 置 1,表示处于内核态。

  3. 在 Cause 寄存器中,保存 ExcCode 段。由于此处是中断异常,对应的异常码即为 0。

  4. PC 转入异常分发程序入口。

  5. 通过异常分发,判断出当前异常为中断异常,随后进入相应的中断处理程序。在MOS 中即对应 handle_int 函数。

  6. 在中断处理程序中进一步判断 CP0_CAUSE 寄存器中是由几号中断位引发的中断,然后进入不同中断对应的中断服务函数。

  7. 中断处理完成,将 EPC 的值取出到 PC 中,恢复 SR 中相应的中断使能,继续执行。

时钟中断的概念:

时钟中断和操作系统的时间片轮转算法是紧密相关的。每个进程都有一个时间片,时间片到了就会被挂起。

gexemul模拟时钟中断:

kclock_init 函数完成了时钟的初始化,该函数主要调用 set_timer 函数,完成如下操作:

Exercise 3.14

set_timer();

Thinking 3.9

关注set_timer和timer_irq函数

#kclock_asm.S
#include <asm/regdef.h>
#include <asm/cp0regdef.h>
#include <asm/asm.h>
#include <kclock.h>


.macro setup_c0_status set clr
#定义宏,作用是将SR寄存器的set位置1,clr位置0
   .set   push#保存初始状态
   mfc0    t0, CP0_STATUS#拿到12号寄存器
   or  t0, \set|\clr
   xor t0, \clr
   mtc0    t0, CP0_STATUS
   .set   pop#恢复初始状态
.endm

   .text
LEAF(set_timer)

   li t0, 0xc8 #11001000,每秒钟中断200次
   sb t0, 0xb5000100#设置时钟中断的频率
   sw  sp, KERNEL_SP#将栈指针存入KERNEL_SP中
setup_c0_status STATUS_CU0|0x1001 0#     0x1001 -> 0001 0000 0000 0001
#STATUS_CU0 0x1000 0000 将SR的第28位,第12位,第0位置1,允许用户态使用CP0,允许4号中断(时钟中断),允许外部中断
   jr ra

   nop
END(set_timer)

#genex.S

#include <asm/regdef.h>
#include <asm/cp0regdef.h>
#include <asm/asm.h>
#include <stackframe.h>

.macro  __build_clear_sti
   STI
.endm
#../include/stackframe.h
.macro STI
   mfc0    t0, CP0_STATUS #12号寄存器
   li  t1, (STATUS_CU0 | 0x1) #
   or  t0, t1#将SR寄存器的28位和第0位置1
   mtc0    t0, CP0_STATUS

.endm
#
.macro  __build_clear_cli
   CLI
.endm
#
.macro CLI
   mfc0    t0, CP0_STATUS
   li  t1, (STATUS_CU0 | 0x1)
   or  t0, t1
   xor t0, 0x1 #将SR寄存器的第28位置1,将SR寄存器的第0位置0
   mtc0    t0, CP0_STATUS
.endm
#
.macro BUILD_HANDLER exception handler clear
   .align 5
  NESTED(handle_\exception, TF_SIZE, sp)
   .set   noat

nop

   SAVE_ALL
   __build_clear_\clear
   .set   at
   move    a0, sp
   jal \handler
   nop
   j   ret_from_exception
   nop
   END(handle_\exception)
.endm

FEXPORT(ret_from_exception)
   .set noat
   .set noreorder
   RESTORE_SOME
   .set at
   lw  k0,TF_EPC(sp)
   lw  sp,TF_REG29(sp) /* Deallocate stack */
//1:   j   1b
   nop
   jr  k0
   rfe



.set noreorder
.align 5
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 #0x1000,第12位
bnez    t1, timer_irq
nop
END(handle_int)

   .extern delay

timer_irq:

   sb zero, 0xb5000110#acknowledge a time irq
1: j   sched_yield#调用sche_yield函数
   nop
   /*li t1, 0xff
  lw   t0, delay
  addu t0, 1
  sw t0, delay
  beq t0,t1,1f    
  nop*/
   j   ret_from_exception
   nop

LEAF(do_reserved)
END(do_reserved)

   .extern tlbra
.set   noreorder



BUILD_HANDLER reserved do_reserved cli
BUILD_HANDLER tlb   do_refill   cli
BUILD_HANDLER mod   page_fault_handler cli
NESTED(do_refill,0 , sp)
          //li   k1, '?'
          //sb   k1, 0x90000000
           .extern mCONTEXT   //存储了一级页表的基地址

1:         //j 1b
           nop
           lw      k1,mCONTEXT
           and     k1,0xfffff000// 保留高20位
           mfc0    k0,CP0_BADVADDR
           srl     k0,20 //右移20位
           and     k0,0xfffffffc //保留高30位
           addu    k0,k1 //高20位为mCONTEXT的高20位,中间10位为虚拟地址的前十位,后2位为0

           lw      k1,0(k0) //提取出一级页表项中的物理地址
           nop
           move    t0,k1
           and     t0,0x0200 //提取其中的标记位
           beqz    t0,NOPAGE //为0表示有问题,跳转
           nop
           and     k1,0xfffff000 //保留高20位
           mfc0    k0,CP0_BADVADDR
           srl     k0,10 //右移10位
           and     k0,0xfffffffc //舍去后两位
           and     k0,0x00000fff //保留低12位 (感觉等效于右移12位,再左移两位)
           addu        k0,k1

           or      k0,0x80000000 //高位补充1,将二级页表项的物理地址转换为虚拟地址,之后交给CPU
           lw      k1,0(k0) //提取出二级页表项内的 物理地址
           nop
           move    t0,k1
           and     t0,0x0200 //看一下有效位
           beqz    t0,NOPAGE //为0无效,跳转
           nop
           move    k0,k1
           and     k0,0x1 //看一下最低位,暂时忽略
           beqz    k0,NoCOW
           nop
           and     k1,0xfffffbff
NoCOW:
mtc0    k1,CP0_ENTRYLO0
           nop
           tlbwr //重填入TLB

           j       2f //跳转至2
           nop
NOPAGE:
//3: j 3b
nop
           mfc0    a0,CP0_BADVADDR
           lw      a1,mCONTEXT
           nop

           sw      ra,tlbra
           jal     pageout//调用pageout函数
           nop
//3: j 3b
nop
           lw      ra,tlbra
           nop
           nop

           j       1b
2:          nop

           jr      ra
           nop
END(do_refill)
BUILD_HANDLER reserved do_reserved cli
BUILD_HANDLER tlb   do_refill   cli
BUILD_HANDLER mod   page_fault_handler cli
.macro get_sp
   mfc0    k1, CP0_CAUSE
   andi    k1, 0x107C #1 0000 0111 1100
   xori    k1, 0x1000 #1 0000 0000 0000 关注k1的2-6以及12位,2-6是否为1,12是否为0
   bnez    k1, 1f
   nop
   li  sp, 0x82000000 #全部为0,表示时钟中断
   j   2f
   nop
1:
   bltz    sp, 2f #第12位为0,表示未开时钟中断
   nop
   lw  sp, KERNEL_SP
   nop

2:  nop


.endm#时钟中断令sp为TIMESTACK,非时钟中断令sp为KERNEL_SP

进程调度:

handle_int 函数的最后跳转到了 sched_ yield 函数。这个函数在lib/sched.c中所定义,它就是我们本次实验最后要写的调度函数。

算法:

调度的算法很简单,就是时间片轮转的算法。env 中的优先级在这里起到了作用,我们规定其数值表示进程每次运行的时间片数量。不过寻找就绪状态进程不是简单遍历进程链表,而是用两个链表存储所有参与调度进程。当进程被创建时,我们要将其插入第一个进程调度链表的头部。调用 sched_yield函数时,先判断当前时间片是否用完。如果用完,将其插入另一个进程调度链表的尾部。之后判断当前进程调度链表是否为空。如果为空,切换到另一个进程调度链表。

void sched_yield(void)
{

   static int count = 0; // remaining time slices of current env
   static int point = 0; // current env_sched_list index
   static struct Env *e = NULL;

       if(count == 0 || e == NULL || e->env_status != ENV_RUNNABLE){
       if(e != NULL){//count == 0 || status is not runnable
           LIST_REMOVE(e, env_sched_link);
           if(e->env_status != ENV_FREE){
               LIST_INSERT_TAIL(&env_sched_list[1-point], e, env_sched_link);
          }
      }
       while(1){
           while(LIST_EMPTY(&env_sched_list[point]))
               point = 1 - point;
           e = LIST_FIRST(&env_sched_list[point]);
           if(e->env_status == ENV_FREE){
               LIST_REMOVE(e, env_sched_link);
          } else if(e->env_status == ENV_NOT_RUNNABLE){
               LIST_REMOVE(e, env_sched_link);
               LIST_INSERT_TAIL(&env_sched_list[1-point], e, env_sched_link);
          } else {
               count = e->env_pri;
               break;
          }
      }
  }

   count --;
   env_run(e);
   //env_run(LIST_FIRST(&env_sched_list[0]));
}
 
posted @ 2022-05-19 18:43  重结晶  阅读(462)  评论(0编辑  收藏  举报