北航OSLab3

北航osLab3笔记

对Lab2的一些修改

在Lab2中,我在page_init里插入物理页框似乎用的是LIST_INSERT_TAIL,应该修改为LIST_INSERT_HEAD,这样一来,内存地址就从高位开始分配。

不过为什么一定要从高位开始分配内存,我也不明白。

进程控制块结构

进程控制块(PCB)包含一个进程运行所需要的各种信息,与进程一一对应。我们来看看进程lab3中涉及到的进程控制块内容

struct Env{
    struct Trapframe env_tf;
    LIST_ENTRY(Env) env_link;
    u_int env_id;
    u_int env_parent_id;
    u_int env_status;
    Pde* env_pgdir;
    u_int env_cr3;
    LIST_ENTRY(Env) env_sched_link;
    u_int env_pri;
    u_int env_runs;
    
    /*其他属性这个实验暂时用不到*/
}

这里所有的属性指导书都有说明,但是有几个地方要特别提醒一下。

env_sched_link是就绪进程链表,以供进程调度使用,env_link是自由进程链表,以供进程分配使用,在对链表进行操作时,千万不要弄错。

env_runs记录了进程究竟运行了几次,尽管这次实验不在乎这个属性,但是还要记得在运行进程时设置这个属性,省的之后实验在回来添加

进程与中断

初始化进程

我们要想运行一个进程,必须先在内核中完成一些初始化工作,为分配和运行进程做准备。

1、

为了分配进程,首先要给初始化一个数组envs储存所有的进程,并将其放入内存中

/*初始化envs*/
envs = (struct Env*)alloc(NENV*sizeof(struct Env),BY2PG,1);
n = ROUND(NENV * sizeof(struct ENV),BY2PG);
boot_map_segment(pgdir, UENVS, n, PADDR(envs), PTE_R);
2、

接下来要建立链表,为进程分配,调度做准备。

/*建立自由进程链表,来为分配做准备*/
LIST_INIT(&env_free_list);
/*建立进程调度链表,为进程调度做准备,
 *之所以是两个链表,是为了调度函数做准备*/
LIST_INIT(&env_sched_list[0]);
LIST_INIT(&env_sched_list[1]);
/*这里的目的是让env_free_list中进程的顺序和envs中的一样,
 *这样一来,第一次取出的进程控制块就是envs[0],
 *可是我在自己测试的时候发现,
 *第一次取出的块究竟是多少并不影响运行,
 *所以这样做的原因不太明白*/
for(i=NENV-1;i>=0;i--){
    envs[i].env_status=ENV_FREE;
    LIST_INSERT_HEAD(&env_free_list,&envs[i],env_link);
}

到这里,我们就做好了初始化进程的准备

创建进程

创建进程包括三步骤,也就是分配控制块,设置优先级,读取代码。放到代码里是这样的

/*binary是我们要读取的代码内容,用elf文件的格式写成
 *size是代码的长度,也就是读取多少字节
 *priority是这个进程的优先级*/
void env_create_priority(u_char *binary,int size,
                        int priority){
    struct Env *e;
    /*分配进程控制块*/
	env_alloc(&e,0);
    /*设置优先级,一句话的事,不解释了*/
	e->env_pri=priority;
    /*读取elf内容*/
	load_icode(e,binary,size);
}

我们一步步看

分配进程控制块

接下来我们分配一个进程控制块,这就需要设置进程控制块的诸多属性,我们一个个来看。

1、
/*env_alloc第一步*/
/*取链表首个控制块*/
e = LIST_FIRST(&env_free_list);
/*如果没有多余的控制块,就返回错误*/
if(e == NULL){
    *new = NULL;
    return -E_NO_FREE_ENV;
}
2、
/*env_alloc第二步*/
int r;
/*设置进程控制块虚拟内存*/
r = env_setup_vm(e);
if(r<0){
    return r;
}

这里却涉及到进程控制块虚拟内存地址空间设置的问题,完成这一部分的是函数 env_setup_vm,我们来看看这个函数

/*env_setup_vm*/
/*首先要分配物理页用来存放页目录*/
r = page_alloc(&p);
/*分配失败,返回错误*/
if(r<0){
    panic("say something you like \n");
    return r;
}
/*成功,就增加一个引用*/
p->pp_ref++;
/*得到这个物理页的虚拟地址*/
pgdir = (Pde *)page2kva(p);

/*设置页表,
 *部分清零,部分直接从boot_pgdir中拷贝*/
for(i=0;i<PDX(UTOP);i++){
    *(pgdir+i) = 0;
}
for(i=PDX(UTOP);i<PTE2PT;i++){
    if(i==PDX(UVPT)){
        continue;
    }
    *(pgdir + i) = *(boot_pgdir+i);
}
/*设置进程有关虚拟地址的属性*/
/*记录页目录虚拟地址*/
e->env_pgdir = pgdir;
/*记录页目录物理地址*/
e->env_cr3 = PADDR(pgdir);
/*设置页目录中UVPT一项*/
e->env_pgdir[PDX(UVPT)] = e->env_cr3 | PTE_V | PTE_R;

这里说一下为什么这样设置页表。

​ 我们的虚拟内存空间使用的是2G/2G模式,UTOP以下是进程自己使用的,在还没用的情况下自然是清零。ULIM以上是内核使用的,自然应该从boot_pgdir中直接拷贝。主要是中间一部分,分别是User VPTPAGESENVS。这三部分也是各有用处,PAGES对应的是之前物理页框数组pagesENVS对应的是进程数组envs,也要从boot_pgdir中拷贝,而User VPT对应的是进程自己的页表,需要之后手动设置。

​ 这样一来,我们每个用户进程都有相同的虚拟地址空间 ENVS,就可以查看其它进程的信息了(当然修改还是修改不了的,因为别处的虚拟地址空间不一样)。

​ 注意User VPTPAGESENVS对于一般用户进程时只能读取不能写入的。

​ 这里多嘴提一下cr3寄存器就是保存进程页目录物理地址的寄存器。

3、

​ 完成了env_setup_vm的调用,回来看下一步,我们要设置各个属性。

/*接着env_alloc第二步*/
/*env_alloc 第三步*/
e->env_id=mkenvid(e);
e->env_status = ENV_RUNNABLE;
e->env_parent_id=parent_id;
e->env_tf.regs[29]=USTACKTOP;
e->env_runs=0;
/*这一句原因详见指导书,
 *这里实验中因为这一句是直接给出来的,
 *所以单独列为第四步,我觉得合并到第三步也可以*/
e->env_tf.cp0_status=0x10001004;

这几个应该都很简单就不多说了。

4、

接下来把这个分配好的进程控制块从env_free_link里删除,再加入env_sched_list

/*env_alloc最后一步*/
LIST_REMOVE(e,env_link);
/*加入进程就绪链表,这里默认加入到0号链表*/
LIST_INSERT_HEAD(&env_sched_list[0],e,env_sched_link);

​ 这里补充一下,有些人觉得尽管初始化了进程控制块,实际上这个进程控制块还没有对应的程序代码,所以不应该加入进程就绪链表,而应该等读取了程序代码再说(读取代码见之后)。

​ 我认为,首先我们在这一步把进程的状态设置为ENV_RUNNABLE那么就应该让它加入进程就绪链表,原则上来讲,所有状态为ENV_RUNNABLE的进程都应该在进程就绪链表里,尽管这个进程可能一句代码都不执行,但是它依旧是一个就绪的进程。

​ 其次,实际角度来讲,也就是我们评测机测试时候会新建一个一句代码都没有的空进程,在实际应用不太可能有这样的进程,如果有人跑了一个空进程,那么报错也是应该的。

读取代码

​ 代码是elf格式写成的,我们按照elf文件中指明的内容加载入内存便是了,这里代码比较复杂。

1、

​ 先看函数load_icode的内容。

struct Page* p=NULL;
u_long r;
u_long perm;
u_long entry_point;
/*load_icode第一部分*/
/*申请一个页框,来初始化这个进程的栈*/
r=page_alloc(&p);
if(r<0){
    return;
}
/*把之前申请的页框p和栈顶虚拟地址对应起来,
 *注意,栈是从高到低增长的,
 *所以最高位的页框,虚拟地址的基地址应该是USTACKTOP-BY2PG
 *如果你没写这一句,你会发现进程第一次运行的时候多一行pageout
 *这是因为pageout在读到没有物理页框的虚拟地址时会
 *自动给它分配一个页框。*/
r=page_insert(e->env_pgdir,p,USTACKTOP-BY2PG,perm);
if(r<0){
    return;
}

/*解析对应的elf文件并读取它进入内存*/
load_elf(binary,size,&entry_point,(void *)e,load_icode_mapper);
/*设置进程的pc寄存器,这里把它设置为程序入口,
 *入口由elf文件决定。*/
e->env_tf.pc = entry_point;

​ 这里有一个函数load_elf是专门用来解析elf文件的,我们来看看

2、

我们找到load_elf函数,这里我们特别注意一个参数

int (* map) (u_long va, u_int32_t sgsize,
            u_char *bin, u_int32_t bin_size,void *user_data)

这是一个函数指针,我们需要分辨出指向函数的指针和返回值为指针的函数。

/*这是一个指针,
 *指向一个函数*/
int (*function1)(int a,int b);
/*这是一个函数,
 *它的返回值为指针int*
 */
int *function(int a,int b);

更详细的内容可以参见《C Primer Plus》

然后我们看函数内容

/*判断是否是elf文件*/
if(size<4 || !is_elf_format(binary)){
    return -1;
}

/*我们要读取的是程序节,所以从程序头解析*/
/*找到程序头表*/
ptr_ph_table = binary + ehdr->e_phorr;
ph_entry_count = ehdr->e_phnum;
ph_entry_size=ehdr->e_phentsize;
/*用循环读出每个程序头*/
while(ph_entry_count--){
    phdr = (ELF32_Phdr *)ptr_ph_table;
    /*type为PT_LOAD说明这个段是一个可执行的段,
     *我们需要读取它,其他类型的段有别的用处,
     *其他类型有兴趣可以查资料了解*/
    if(phdr->p_type == PT_LOAD){
        /*调用之前参数里的函数,参照load_icode
         *我们知道这个函数是load_icode_mapper
         *负责读取一个程序段,这里注意memsz对应的是sgsize
         *filesz对应的是bin_size*/
        r=map(phdr->p_vaddr,phdr->memsz,
             phdr->p_offset+binary,phdr->p_filesz,
             user_data);
        /*如果读取失败,则返回*/
        if(r<0){
            return r;
        }
    }
    /*找到下一个程序段,这里注意
     *ptr_ph_table是unsigned char* 类型
     *所以偏移地址是以字节为单位*/
    ptr_ph_table+=ph_entry_size;
}
/*指定程序入口
 *还记得不,我们之前把pc寄存器设置为entry_point
 *这个entry_point就是这里指定的*/
*entry_point = ehdr->e_entry;

通过这个我们看到,只要再用load_icode_mapper把elf的每个程序节读入内存就可以了

3、

接下来我们看看用来读取程序段到内存的函数load_icode_mappere

参数bin_sizesgsize。前者代表ELF文件中程序代码的长度,后者代表这段程序在内存中应该有的内存。sgsize有时候大于bin_size,因为程序在运行过程中可能产生新的数据,这些数据就储存在多出来的区域中。

还有一个参数是user_data,这个参数是目标进程,如果我们要加载代码的进程是进程A,那么user_data就是进程A的进程控制块指针。

好了,接下来我们看函数内容。

我们先把elf文件内容读入内存,如果sgsize大于bin_size,那么就用0把这些位置填满。

这里必须理解读入内存时遇到的多种情况,我们先看读入bin_size的情况,读入sgsize的情况与之相似。

大致分为三种,这里只画出了va不在页面开头的情况,va也有可能在页面开头。

在这里插入图片描述

根据这三种情况我们这样填写

struct Env *env = (struct Env*)user_data;
/*如果va不在开头,那么求出va相对于页面开头的偏移*/
u_long offset = va-ROUNDDOWN(va,BY2PG);
/*得到进程的页目录*/
Pde* pgdir = env->env_pgdir;
int size=0;
/*如果offset>0那么显然va不在开头*/
if(offset>0){
    /*检查va是否已经对应了一个物理页面*/
    p=page_lookup(pgdir,va,NULL);
    /*如果没有,就分配一个*/
    if(p==NULL){
        r=page_alloc(&p);
        if(r<0){
            return r;
        }
        page_insert(pgdir,p,va,PTE_R);
    }
    /*这里比较页面p剩余内存大小和bin_size,
     *如果页面剩余大小就能装下bin_size
     *那就是情况(1),这样一来就直接全拷贝进去就可以了*/
    size = BY2PG-offset;
    size = (bin_size<size)?bin_size:size;
    bcopy((void *)bin,(void *)(page2kva(p)+offset),size);
}
/*这里我们考虑,如果内存是第一种情况,那么size==bin_size,
 *此时不会进入循环,如果是二三种情况,则会进入循环*/
i=size;
for(;i<bin_size;i+=size){
    /*因为这里是从页面的开头开始写的,
     *我们直接申请一个新的页面*/
    r=page_alloc(&p);
    if(r<0){
        return r;
    }
    page_insert(pgdir,p,va+i,PTE_R);
    /*这里size的用法要理解好,
     *i为已经读取了的字节,bin_size-i是还要读取的字节
     *如果BY2PG>(bin_size-i)的话,我们就再写一页,
     *否则,我们就只把剩下的字节写入,
     *size=bin_size-i,这次循环末尾就会把i设置为
     *bin_size,从而跳出循环*/
    size(BY2PG<(bin_size-i))?BY2PG:(bin_size-i);
    bcopy((void *)(bin+i),(void *)(page2kva(p)),size);
}
/*写sgsize有异曲同工之妙,不多解释*/
offset = i-ROUNDDOWN(i,BY2PG);
if(offset > 0){
    p=page_looup(pgdir,(va+i),NULL);
    if(p==NULL){
        r=page_alloc(&p);
        if(r<0){
            return r;
        }
        page_insert(pgdir,p,va+i,PTE_R);
    }
    size=BY2PG-offset;
    size=((sgsize-i)<size)?(sgsize-i):size;
    bzero((void*)(page2kva(p)+offset),size);
    i=i+size;
}
while(i<sgsize){
    r=page_alloc(&p);
    if(r<0){
        return r;
    }
    page_insert(pgdir,p,va+i,PTE_R);
    size=(BY2PG<(sgsize-i))?BY2PG:(sgsize-i);
    bzero((void *)page2kva(p),size);
    i=i+size;
}

这样elf的程序节就读取完成了

4、

我们再来回顾下调用的规律,load_icode调用load_elf调用load_icode_mappere

用宏创建进程

在实验中,我们是用宏来创建进程的,这些宏函数被定义在env.h中。

这些函数是ENV_CREATE_PRIORITY(x,y)ENV_CREATE(x,y)ENV_CREATE2(x,y)

还有一个普通函数env_create,内容都很简单,大家自己看看就好。

异常中断处理

我们接下来要了解进程的运行,但是进程和异常中断是脱不了干系的。为了更好地理解进程运行时的代码,我们先看看如何处理异常中断。

在这次实验中,我们只有一种中断就是时钟中断。

mips_init中我们有一句

trap_init();

这句话初始化了处理异常的数据。我们来看看这部分代码

/*exception_handlers储存了异常处理函数,
 *把异常和处理异常的函数对应起来
 *称为异常向量组*/
unsigned long exception_handlers[32];
void trap_init(){
    int i;
    for(i=0;i<32;i++)
        set_except_vector(i,handle_reserved);
    /*handle_开头的函数是处理对应异常的函数
     *set_except_vector是负责把这也函数的地址
     *放入异常向量组*/
    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);
}

我们本次实验只需要handle_int就可以了。顺便一提,有关pageout的问题,可以看异常处理函数handle_tlb的内容。

异常处理函数
handle_int()

这个函数在文件genex.S里内容如下,

/*读取cause和sp寄存器
 *它们的作用可以看《see mips run linux》*/
NESTED(handle_int, TF_SIZE, sp)
nop

SAVE_ALL
CLI

mfc0 t0, CP0_CAUSE
mfc0 t2, CP0_STATUS
/*这三句用来判断是否可以跳转
 *到调度函数*/
and t0,t2
andi t1, t0, STATUSF_IP4
bnez t1, timer_irq
nop
END(handle_int)

timer_irq:
/*sched_yield是进程调度函数
 *后面再说*/
1:	j sched_yield
	nop
	j ret_from_exception
	nop

可见这个处理函数就是读取协处理器CP0的内容,然后根据CP0判断是否进行进程调度。

这里有两个宏需要解释一下,分别是CLISAVE_ALL

/*这个宏设置SR寄存器,让CPU禁止中断*/
.macro CLI
	mfc0 t0,CP0_STATUS
	li t1,(STATUS_CP0|0x1)
	or t0, t1
	xor t0, 0x1
	mtc0 t0, CP0_STATUS
.endm
.macro SAVE_ALL
/*这三条指令检查SR寄存器,*/
	mfc0 k0,CP0_STATUS
	sll k0,3
	bltz k0,1f
	nop

1:
              	move    k0,sp
              	/*这个宏设置sp寄存器,
              	 *当cause寄存器不同时不同
              	 *如果ExcCode域为0(异常类型为中断)
              	 *且为4号中断就把sp设置为
              	 *0x82000000,否则就设置为KERNEL_SP,
              	 *寄存器更详细情况参照R3000用户手册,
              	 *这里和《see mips run linux》有出入*/
                get_sp
                /*接下来这一长串就是把寄存器的值储存到指定
                 *的栈里面*/
                move    k1,sp
                subu    sp,k1,TF_SIZE
                sw      k0,TF_REG29(sp)
                sw      $2,TF_REG2(sp)
                mfc0    v0,CP0_STATUS
                sw      v0,TF_STATUS(sp)
                mfc0    v0,CP0_CAUSE
                sw      v0,TF_CAUSE(sp)
                mfc0    v0,CP0_EPC
                sw      v0,TF_EPC(sp)
                mfc0    v0, CP0_BADVADDR
                sw      v0, TF_BADVADDR(sp)
                mfhi    v0
                sw      v0,TF_HI(sp)
                mflo    v0
                sw      v0,TF_LO(sp)
                sw      $0,TF_REG0(sp)
                sw      $1,TF_REG1(sp)
                sw      $2,TF_REG2(sp)
                sw      $3,TF_REG3(sp)
                sw      $4,TF_REG4(sp)
                sw      $5,TF_REG5(sp)
                sw      $6,TF_REG6(sp)
                sw      $7,TF_REG7(sp)
                sw      $8,TF_REG8(sp)
                sw      $9,TF_REG9(sp)
                sw      $10,TF_REG10(sp)
                sw      $11,TF_REG11(sp)
                sw      $12,TF_REG12(sp)
                sw      $13,TF_REG13(sp)
                sw      $14,TF_REG14(sp)
                sw      $15,TF_REG15(sp)
                sw      $16,TF_REG16(sp)
                sw      $17,TF_REG17(sp)
                sw      $18,TF_REG18(sp)
                sw      $19,TF_REG19(sp)
                sw      $20,TF_REG20(sp)
                sw      $21,TF_REG21(sp)
                sw      $22,TF_REG22(sp)
                sw      $23,TF_REG23(sp)
                sw      $24,TF_REG24(sp)
                sw      $25,TF_REG25(sp)
                sw      $26,TF_REG26(sp)
                sw      $27,TF_REG27(sp)
                sw      $28,TF_REG28(sp)
                sw      $30,TF_REG30(sp)
                sw      $31,TF_REG31(sp)
.endm
异常分发

​ 上面我们设置好了异常处理函数,还需要让CPU在异常发生的时候跳到异常处理函数中,这就涉及到异常分发的过程。CPU在异常发生的时候,会自动跳转到一个特定的地址(这个地址由硬件自己决定),对于mips,这个地址是0x8000 0080。所以我们要编写用来分发异常的函数并且把这个函数放到0x8000 0080地址上。

/*我们在start.S中添加异常分发代码*/
NESTEN(except_vec3,0,sp)
/*我也不知道这两句是干啥的*/
	.set noat
	.set noreorder
1:
	mfc0 k1,CP0_CAUSE
	la k0,exception_handlers
	/*我们通过这个操作,把cause寄存器
	 *中的2-6位(记为ExcCode)取出来放入k1寄存器
	 *时钟中断对应的ExcCode为000000*/
	andi k1,0x7c
	/*把ExcCode和exception_handlers相加
	 *得到与异常对应的异常处理函数地址,
	 *这里注意,现在k0是一个内存地址,
	 *这个内存地址中储存的数才是异常处理函数入口的位置*/
	addu k0,k1
	/*把异常处理函数入口位置读入k0*/
	lw k0,(k0)
	NOP
	/*跳转到异常处理函数*/
	jr k0
	nop

这就是整个异常分发的过程。

我们再在scse0_3.lds中把这段代码加入到指定位置便可。

更多有关cause寄存器的知识可以看看《See MIPS Run Linux》

进程运行

现在我们来看看进程的运行。

我们启动一个新的进程,往往意味着要阻塞一个旧的进程,所以我们先要保存旧进程的运行环境,由于每个进程都有自己的地址空间,进程的内存数据不需要再额外保存了,但是进程的寄存器数据必须保存起来。

然后我们再把要运行的进程加载进来。

1、

让我们看看env_run的内容

void env_run(struct Env *e){
    /*检查当前有没有进程正在运行
     *如果有,就把当前进程状态保存*/
    if(curenv!=NULL){
        /*如果当前有进程在进行,那么就先保存它的寄存器
         *在中断时的状态到env_tf中,
         *这里的TIMESTACK就是0x82000000,
         *这个常数就是发生中断时储存寄存器内容的栈*/
        bcopy((void*)TIMESTACK-sizeof(struct Trapframe),
             &(curenv->env_tf),
             sizeof(struct Trapframe));
        /*我们把pc设置为epc寄存器的值
         *这里epc寄存器储存的是遭受异常的指令地址
         *一般在处理完异常后我们会重新执行发生异常的
         *指令,所以要把pc寄存器设置为epc,
         *但是在Cause寄存器的31位为1时,会把epc
         *设置为异常指令之前的一条指令,可以参照
         *《See MIPS Run Linux》*/
        curenv->env_tf.pc=curenv->env_tf.epc
    }
    /*把当前进程切换为e*/
    curenv = e;
    /*env_runs代表进程被运行的次数,这里让它加一*/
    curenv->env_runs++;
    /*把新进程的地址空间设置好*/
    lcontext(e->env_pgdir);
    /*读入新进程的寄存器数据,恢复现场*/
	env_pop_tf(&(e->env_tf),GET_ENV_ASID(e->env_id));   
}

我们发现,这里我们调用了几个函数lcontextenv_pop_tfGET_ENV_ASID

2、

我们先看看lcontext,这个函数用来设置进程的地址空间,是一个简单的汇编函数。

LEAF(lcontext)
	.extern mCONTEXT
	sw a0,mCONTEXT
	jr ra
	nop
END(lcontext)

其实就是把参数写入mCONTEXT,这个变量储存的是当前进程页目录的虚拟地址,在出现缺页异常的时候会用到它。

3、

现在我们看看env_pop_tf函数

LEAF(env_pop_tf)
.set    mips1

		nop
                move            k0,a0
                /*a1是第一个参数,代表
                 *GET_ENV_ASID(e->env_id)
                 *这里是把这个值写入EntryHi寄存器
                 *我们使用的硬件EntryHi寄存器11-6位
                 *是ASDI域,它是地址空间的标识符,
                 *每个进程都有不同的地址空间标识符
                 *这里还是要看R3000手册上的内容*/
                mtc0    a1,CP0_ENTRYHI
                /*接下来这四条指令,就是把SR寄存器最低两位
                 *清空,不进入异常级但是禁止全局中断
                 *防止在设置寄存器时被中断。*/
                mfc0    t0,CP0_STATUS
                ori     t0,0x3
                xori    t0,0x3
                mtc0    t0,CP0_STATUS
				/*下面这一段都是在重新设置寄存器*/
                lw      v1,TF_LO(k0)
                mtlo    v1
                lw      v0,TF_HI(k0)
                lw      v1,TF_EPC(k0)
                mthi    v0
                mtc0    v1,CP0_EPC
                lw      $31,TF_REG31(k0)
                lw      $30,TF_REG30(k0)
                lw      $29,TF_REG29(k0)
                lw      $28,TF_REG28(k0)
                lw      $25,TF_REG25(k0)
                lw      $24,TF_REG24(k0)
                lw      $23,TF_REG23(k0)
                lw      $22,TF_REG22(k0)
                lw      $21,TF_REG21(k0)
                lw      $20,TF_REG20(k0)
                lw      $19,TF_REG19(k0)
                lw      $18,TF_REG18(k0)
                lw      $17,TF_REG17(k0)
                lw      $16,TF_REG16(k0)
                lw      $15,TF_REG15(k0)
                lw      $14,TF_REG14(k0)
                lw      $13,TF_REG13(k0)
                lw      $12,TF_REG12(k0)
                lw      $11,TF_REG11(k0)
                lw      $10,TF_REG10(k0)
                lw      $9,TF_REG9(k0)
                lw      $8,TF_REG8(k0)
                lw      $7,TF_REG7(k0)
                lw      $6,TF_REG6(k0)
                lw      $5,TF_REG5(k0)
                lw      $4,TF_REG4(k0)
                lw      $3,TF_REG3(k0)
                lw      $2,TF_REG2(k0)
                lw      $1,TF_REG1(k0)
                /*到这里把所有寄存器都设置完毕了*/
                /*读出进程中pc寄存器值和SR寄存器值*/
                lw      k1,TF_PC(k0)
                lw      k0,TF_STATUS(k0)
                nop
                /*写入SR寄存器*/
                mtc0    k0,CP0_STATUS
                /*跳转到新进程的pc值*/
                j       k1
                /*这个指令在跳转指令的延迟槽里,
                 *依旧能够执行。*/
                rfe
                nop
END(env_pop_tf)

至此,一个进程算是跑起来了。

调度函数

最后我们提一下调度函数。

这个函数并不复杂,只是有一点,我们要考虑到调度的进程需要的时间片为零的情况,也就是说这个进程在建立之初就不需要运行,但是依旧在进程就绪链表中存在的状况。

考虑到这一点,就可以比较简单的写出来了。

PS

KERNEL_SP

我们知道TIMESTACK是用来储存中断时进程寄存器状态的,但是还有一个KERNEL_SP还没有提过。

它在env_asm.S中声明

	.global KERNEL_SP;
KERNEL_SP:
	.word 0

这里相当于声明了一个32位的全局变量,并把它赋初值为0

set_timer中有一句

sw sp, KERNEL_SP

显然这里是把sp寄存器的值存入KERNEL_SP中,由于set_timer是由内核调用的,我们也不难看出,KERNEL_SP是用来储存内核栈的地址的。

然后我们看看用处。

在文件stackframe.h中有一个宏get_sp,大概干了这么几件事:

  1. 读取cuase寄存器,判断一下是不是4号中断,如果是就设置sp寄存器为TIMESTACK

  2. 如果不是就设置sp寄存器为KERNEL_SP中的数值。

    lw sp, KERNEL_SP
    

这个宏我们之前在SAVE_ALL中用过了,显然它在异常发生时设置sp寄存器,来实现不同异常发生时的内容存到不同栈里的效果。

参考代码

感谢两位大佬
https://github.com/JamesDYX/BUAA_OS_Lab
https://github.com/login256/BUAA-OS-2019

  • 6
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值