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 = (struct Env *)alloc(NENV * sizeof(struct Env), BY2PG, 1);
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;
}
本函数(开辟新的进程控制块)步骤:
-E_NO_FREE_ENV
。否则从LIST_FIRST取一个进程控制块e。env_setup_vm(e)
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;
}
本函数步骤:
PDX(UTOP)
项清空置零 (Q1:为什么是前PDX(UTOP)
项?Q2: 为什么需要置零?) 。 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 |
为什么要将这部分也映射给内核呢?
pgdir
和物理地址(可以由PADDR宏,或者page2pa宏得到)都赋值给进程结构体e。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;
}
本函数步骤:
Thinking:为什么左移11?
我觉得是因为,后面有个函数envid2env()
,在计算envid的索引时用的宏ENVX
取envid的后十位。也就是说,我觉得可能envid的后十位才表示他的id,如果不左移11位的话,第十位是1,后几位是index,而我们的index不需要前面的1,所以要用左移将它除去。如果没有这个1的话,就无法左移获得一个10位(11位)的数。
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;
}
本函数步骤:
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号,而仅仅是进程的物理位置与另一个进程相同。
curenv
是不是有合法perm去操作这个特定进程(要么e是当前进程本身e != curenv
,要么e是它的直接子进程e->env_parent_id != curenv->env_id
)。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;
}
本函数主要步骤:
page_insert()
将物理页p
和虚拟地址USTACKTOP - BY2PG
联系起来,初始化一个进程的栈,表示常规用户栈normal user stack里一个4KB的一页的空间被使用。load_elf()
函数将每个segment都加载到正确的地方entry_point
将每个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循环中实现的。主要有两步:
ptr_ph_table
递增一个entry_size的大小。由map函数,即load_icode_mapper()
函数实现。void* user_data
这个参数是一个函数指针。
函数指针:可以给不同的需要加载的内容动态选择合适的mapper函数。(OSLAB中只有一个mapper函数,其实可以有多个)
mapper函数是把UTEXT的部分映射到新开的page里。
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);
}
本函数主要创建一个进程,步骤:
env_alloc(&e, 0)
函数中完成。Thinking: 这里为什么
env_alloc
函数的第二个参数是0?
这是一种默认做法。
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));
}
本函数主要负责进程的切换,步骤:
*old
就指向(TIMESTACK - sizeof(struct Trapframe));
这意思在栈顶开一个tf大小的空间。然后将上下文保存到当前进程的 curenv->env_tf
中。Thinking: 关于
li sp, 0x82000000
这是在stackframe.h的一句汇编。一个get_sp的宏。我们本次做的都是时钟中断,所以说,存储上下文寄存器的栈指针sp指向的是时钟栈区TIMESTACK。
lcontext()
汇编函数切换地址:将mCONTEXT
(页目录首地址)存到a0。并跳转到ra寄存器进程调度主要是sched_ yield函数完成的。
调度算法是时间片轮转,在我们的实验中,优先级并不是传统理解中的优先级,而是时间片长度。
. = 0x80000080;
.except_vec3 : {
*(.text.exc_vec3)
}
首先是进入异常处理程序的入口,一旦CPU发生异常,就自动跳转到0x8000_0080,这里放的是.text.exc_vec3代码。
.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这个句柄。
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。
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 -》若当前进程用完时间片-》找一个新的进程并运行-》若当前进程没有用完时间片-》继续运行当前进程。
详细来说见注释。
此外,