北航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 VPT
、PAGES
、ENVS
。这三部分也是各有用处,PAGES
对应的是之前物理页框数组pages
,ENVS
对应的是进程数组envs
,也要从boot_pgdir
中拷贝,而User VPT
对应的是进程自己的页表,需要之后手动设置。
这样一来,我们每个用户进程都有相同的虚拟地址空间 ENVS
,就可以查看其它进程的信息了(当然修改还是修改不了的,因为别处的虚拟地址空间不一样)。
注意User VPT
、PAGES
、ENVS
对于一般用户进程时只能读取不能写入的。
这里多嘴提一下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_size
、sgsize
。前者代表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判断是否进行进程调度。
这里有两个宏需要解释一下,分别是CLI
和SAVE_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));
}
我们发现,这里我们调用了几个函数lcontext
,env_pop_tf
,GET_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
,大概干了这么几件事:
-
读取cuase寄存器,判断一下是不是4号中断,如果是就设置sp寄存器为
TIMESTACK
, -
如果不是就设置sp寄存器为
KERNEL_SP
中的数值。lw sp, KERNEL_SP
这个宏我们之前在SAVE_ALL
中用过了,显然它在异常发生时设置sp寄存器,来实现不同异常发生时的内容存到不同栈里的效果。
参考代码
感谢两位大佬
https://github.com/JamesDYX/BUAA_OS_Lab
https://github.com/login256/BUAA-OS-2019