文章目录
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;
}
本函数(开辟新的进程控制块)步骤:
- 从free_list取一个新的进程控制块:如果free_list已经为空,就返回
-E_NO_FREE_ENV
。否则从LIST_FIRST取一个进程控制块e。 - 然后给这个进程控制块分配虚拟空间。
env_setup_vm(e)
- 初始化进程控制块的一些参数。
- 然后正式将这个进程控制块从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;
}
本函数步骤:
- alloc一个页p,作为页目录。
- 将页目录的前
PDX(UTOP)
项清空置零 (Q1:为什么是前PDX(UTOP)
项?Q2: 为什么需要置零?) 。 - 将内核页目录拷贝到进程页目录。
○ 因为根据./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的用户进程虚拟区,可能是用来给内核一个获得其他进程状态、信息的入口。因此,对于内核来说这部分应该是只读模式。
- 将进程页目录的虚拟地址
pgdir
和物理地址(可以由PADDR宏,或者page2pa宏得到)都赋值给进程结构体e。 - 将进程页目录的表示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;
}
本函数步骤:
- 计算进程索引:第 index 个进程
- 生成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;
}
本函数步骤:
- 如果envid是0,返回当前进程控制块。
- 获得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号,而仅仅是进程的物理位置与另一个进程相同。
- 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;
}
本函数主要步骤:
- 申请一个物理页p,用函数
page_insert()
将物理页p
和虚拟地址USTACKTOP - BY2PG
联系起来,初始化一个进程的栈,表示常规用户栈normal user stack里一个4KB的一页的空间被使用。 - 使用
load_elf()
函数将每个segment都加载到正确的地方 - 将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循环中实现的。主要有两步:
- 加载elf文件中的内容到内存。
○ 这一步中,先判断这个phdr是不是loadable的。如果可被加载,再加载。
○ 然后ptr_ph_table
递增一个entry_size的大小。
○ phdr指向下一个元素。 - 内存富余空间填零。(Q: ???where)
- 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);
}
本函数主要创建一个进程,步骤:
- 分配一个新的Env结构体。
- 设置进程控制块,给进程分配虚拟空间。
以上两步在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));
}
本函数主要负责进程的切换,步骤:
- 保存当前进程的寄存器到 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。
- 恢复要启动的进程。
○ 将当前进程curenv设置为新进程e。
○ 用lcontext()
汇编函数切换地址:将mCONTEXT
(页目录首地址)存到a0。并跳转到ra寄存器
○ env_pop_tf:把 env 里的 tf 放到寄存器里。
○ 把当前进程的id后5位清空。
lab3-2
进程调度
进程调度主要是sched_ yield函数完成的。
调度算法是时间片轮转,在我们的实验中,优先级并不是传统理解中的优先级,而是时间片长度。
- 在什么时候会调用sched_ yield函数?
- 在env_destroy 函数中。这个函数的主要职责就是free一个进程并且调一个进程来运行。但目前为止还没有调用过这个函数。只是声明且定义了它。
- 时钟中断。
时钟中断的全过程
- 什么时候会开启时钟中断?
- 进入异常。
. = 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。
- 调用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抱有异议)