BUAA_OS_2020_Lab3_Code_Review
(今天终于弄完了lab6的课下测试和实验报告,可以和OSLAB wave goodbye了,耶耶耶)
Lab 3文件树如下,新增文件用*标出,本lab主要与进程管理相关。
. ├── boot │ ├── Makefile │ └── start.S ├── drivers │ ├── gxconsole │ │ ├── console.c │ │ ├── dev_cons.h │ │ └── Makefile │ └── Makefile ├── fs │ └── fsformat ├── gxemul │ ├── elfinfo │ ├── fsformat * │ ├── r3000 │ ├── r3000_test │ ├── test │ └── view ├── include │ ├── args.h │ ├── asm │ │ ├── asm.h │ │ ├── cp0regdef.h │ │ └── regdef.h │ ├── asm-mips3k │ │ ├── asm.h │ │ ├── cp0regdef.h │ │ └── regdef.h │ ├── env.h │ ├── error.h │ ├── fs.h * │ ├── kclock.h │ ├── kerelf.h │ ├── mmu.h │ ├── pmap.h │ ├── printf.h │ ├── print.h │ ├── queue.h │ ├── sched.h │ ├── stackframe.h │ ├── trap.h │ ├── types.h │ └── unistd.h ├── include.mk ├── init │ ├── code_a.c * │ ├── code_b.c * │ ├── init.c │ ├── main.c │ └── Makefile ├── lib │ ├── env_asm.S * │ ├── env.c │ ├── genex.S │ ├── getc.S * │ ├── kclock_asm.S * │ ├── kclock.c * │ ├── kernel_elfloader.c * │ ├── Makefile │ ├── printBackUp │ ├── print.c │ ├── printf.c │ ├── sched.c * │ ├── syscall_all.c * │ ├── syscall.S * │ └── traps.c * ├── Makefile ├── mm │ ├── Makefile │ ├── pmap.c │ └── tlb_asm.S ├── readelf │ ├── kerelf.h │ ├── main.c │ ├── Makefile │ ├── readelf.c │ ├── testELF │ └── types.h └── tools └── scse0_3.lds
在新增的代码中,fs
开头的文件用来支持文件系统(lab5),init
中的两个code.c
文件是模拟要载入的二进制文件,env
开头的文件与进程管理相关(也就是本lab的主要内容),kclock
是控制时钟中断用的文件(在进程调度的时间片轮转法中会用来产生时钟中断),sched.c
是进程调度管理文件。syscall
用来实现系统调用(主要在lab4开始关注),trap.c
用来管理陷入内核相关内容。
进程相关
指导书已经指明,与进程相关的信息都保存在进程控制块中,进程控制块的空间在pmap.c
中分配。
1 /* 2 * ./include/env.h 3 */ 4 5 /* See COPYRIGHT for copyright information. */ 6 7 #ifndef _ENV_H_ 8 #define _ENV_H_ 9 10 #include "types.h" 11 #include "queue.h" 12 #include "trap.h" 13 #include "mmu.h" 14 15 #define LOG2NENV 10 16 #define NENV (1<<LOG2NENV) 17 #define ENVX(envid) ((envid) & (NENV - 1)) 18 #define GET_ENV_ASID(envid) (((envid)>> 11)<<6) 19 20 // Values of env_status in struct Env 21 #define ENV_FREE 0 22 #define ENV_RUNNABLE 1 23 #define ENV_NOT_RUNNABLE 2 24 25 struct Env { 26 struct Trapframe env_tf; // Saved registers 27 LIST_ENTRY(Env) env_link; // Free list 28 u_int env_id; // Unique environment identifier 29 u_int env_parent_id; // env_id of this env's parent 30 u_int env_status; // Status of the environment 31 Pde *env_pgdir; // Kernel virtual address of page dir 32 u_int env_cr3; 33 LIST_ENTRY(Env) env_sched_link; 34 u_int env_pri; 35 // Lab 4 IPC 36 u_int env_ipc_value; // data value sent to us 37 u_int env_ipc_from; // envid of the sender 38 u_int env_ipc_recving; // env is blocked receiving 39 u_int env_ipc_dstva; // va at which to map received page 40 u_int env_ipc_perm; // perm of page mapping received 41 42 // Lab 4 fault handling 43 u_int env_pgfault_handler; // page fault state 44 u_int env_xstacktop; // top of exception stack 45 46 // Lab 6 scheduler counts 47 u_int env_runs; // number of times been env_run'ed 48 u_int env_nop; // align to avoid mul instruction 49 }; 50 51 LIST_HEAD(Env_list, Env); 52 extern struct Env *envs; // All environments 53 extern struct Env *curenv; // the current env 54 extern struct Env_list env_sched_list[2]; // runnable env list 55 56 void env_init(void); 57 int env_alloc(struct Env **e, u_int parent_id); 58 void env_free(struct Env *); 59 void env_create_priority(u_char *binary, int size, int priority); 60 void env_create(u_char *binary, int size); 61 void env_destroy(struct Env *e); 62 63 int envid2env(u_int envid, struct Env **penv, int checkperm); 64 void env_run(struct Env *e); 65 66 67 // for the grading script 68 #define ENV_CREATE2(x, y) \ 69 { \ 70 extern u_char x[], y[]; \ 71 env_create(x, (int)y); \ 72 } 73 #define ENV_CREATE_PRIORITY(x, y) \ 74 {\ 75 extern u_char binary_##x##_start[]; \ 76 extern u_int binary_##x##_size;\ 77 env_create_priority(binary_##x##_start, \ 78 (u_int)binary_##x##_size, y);\ 79 } 80 #define ENV_CREATE(x) \ 81 { \ 82 extern u_char binary_##x##_start[];\ 83 extern u_int binary_##x##_size; \ 84 env_create(binary_##x##_start, \ 85 (u_int)binary_##x##_size); \ 86 } 87 88 #endif // !_ENV_H_
在这个头文件中,首先定义了进程控制块的个数为1024
,也就是说操作系统最多支持同时1024个进程。一个整数的低10位用来编码进程控制块的编号,ENVX
通过取出envid
的低10位,得到进程控制块的编号。
1 #define LOG2NENV 10 2 #define NENV (1<<LOG2NENV) 3 #define ENVX(envid) ((envid) & (NENV - 1)) 4 #define GET_ENV_ASID(envid) (((envid)>> 11)<<6)
然后定义了进程控制块的三种状态:空闲、挂起、可调度。
1 // Values of env_status in struct Env 2 #define ENV_FREE 0 3 #define ENV_RUNNABLE 1 4 #define ENV_NOT_RUNNABLE 2
定义了进程控制块的结构,各个域的含义在注释中给出。
1 struct Env { 2 struct Trapframe env_tf; // 用来保存上下文寄存器的结构体,在trap.h中定义。 3 LIST_ENTRY(Env) env_link; // 空闲链表 4 u_int env_id; // 每个进程独一无二的编号 5 u_int env_parent_id; // 父进程的id 6 u_int env_status; // 进程控制块的状态 7 Pde *env_pgdir; // 进程对应页目录的内核虚拟地址 8 u_int env_cr3; // 进程对应页目录首地址寄存器的值,cr3寄存器不同的值对应不同地址空间 9 LIST_ENTRY(Env) env_sched_link; // 进程调度队列中用来链接的域 10 u_int env_pri; // 进程优先级,体现在时间片轮转法中是进程可以连续占有的时间片个数 11 12 // Lab 4 IPC 13 u_int env_ipc_value; // 进程间通信发送的内容 14 u_int env_ipc_from; // 信息发送者进程的id 15 u_int env_ipc_recving; // 进程是否处于接收信息的状态 16 u_int env_ipc_dstva; // 发送信息的虚拟地址(是某一页内存的首地址) 17 u_int env_ipc_perm; // 发送的内存页进行映射时的权限 18 19 // Lab 4 fault handling 20 u_int env_pgfault_handler; // 缺页中断的handler 21 u_int env_xstacktop; // 异常处理栈的栈顶位置 22 23 // Lab 6 scheduler counts 24 u_int env_runs; // 进程被调度的次数 25 u_int env_nop; // align to avoid mul instruction(似乎是为了对齐额外定义的量) 26 };
其中定义在trap.h
中的struct Trapframe
如下,其实相当于一套MIPS寄存器。
1 struct Trapframe { //lr:need to be modified(reference to linux pt_regs) TODO 2 /* Saved main processor registers. */ 3 unsigned long regs[32]; 4 5 /* Saved special registers. */ 6 unsigned long cp0_status; 7 unsigned long hi; 8 unsigned long lo; 9 unsigned long cp0_badvaddr; 10 unsigned long cp0_cause; 11 unsigned long cp0_epc; 12 unsigned long pc; 13 };
最后定义了三个宏,用来创建进程。第一个宏是评测时用到的,本地运行使用后两个,第二个宏用来创建带优先级的进程,第三个宏用来创建不带优先级的进程。使用方法是:如果想要加载user/
文件夹中的proc.c
对应的进程,就在init.c
中调用ENV_CREATE(user_proc);
即可。在编译时需要先编译出.b
与.x
文件才能创建。
1 // for the grading script 2 #define ENV_CREATE2(x, y) \ 3 { \ 4 extern u_char x[], y[]; \ 5 env_create(x, (int)y); \ 6 } 7 #define ENV_CREATE_PRIORITY(x, y) \ 8 {\ 9 extern u_char binary_##x##_start[]; \ 10 extern u_int binary_##x##_size;\ 11 env_create_priority(binary_##x##_start, \ 12 (u_int)binary_##x##_size, y);\ 13 } 14 #define ENV_CREATE(x) \ 15 { \ 16 extern u_char binary_##x##_start[];\ 17 extern u_int binary_##x##_size; \ 18 env_create(binary_##x##_start, \ 19 (u_int)binary_##x##_size); \ 20 }
了解了以上内容便可以根据指导书补全env.c
中的各个函数代码,env.c
中各个函数的用途为:
u_int mkenvid(struct Env *e)
:产生一个进程控制块号,由于函数内有一个static值,这个值被放在envid
的高位,所以保证了进程控制块号的绝对不重复。(除非整数溢出了并且溢出之后的调用顺序正好和原来一样,不过这种情况…emm…应该不会发生吧)int envid2env(u_int envid, struct Env **penv, int checkperm)
:根据envid
查找env
,地址赋给penv
指针,需要注意的是当envid
=0返回的是curenv
而非null
,void env_init(void)
:用来初始化进程控制模块,行为是初始化了调度队列、空闲队列,以及各个进程控制块。static int env_setup_vm(struct Env *e)
:初始化进程e
对应的内核态内存映射。int env_alloc(struct Env **new, u_int parent_id)
:创建新的进程。static int load_icode_mapper(u_long va, u_int32_t sgsize, u_char *bin, u_int32_t bin_size, void *user_data)
:用来加载二进制文件,这个有巨多参数的函数是本lab最麻烦的一个函数。static void load_icode(struct Env *e, u_char *binary, u_int size)
:一个封装的函数,其调用load_elf
并将load_icode_mapper
作为参数传入,进行调用。void env_create_priority(u_char *binary, int size, int priority)
:根据二进制文件创建一个带有优先级的进程。void env_create(u_char *binary, int size)
:相当于env_create_priority
中priority
=1的情况。这两个创建进程的函数与头文件中定义的宏相对应。void env_free(struct Env *e)
:释放进程与其使用的内存。void env_destroy(struct Env *e)
:封装了env_free
,并且考虑到当前进程被free时调度新进程的情况。extern void env_pop_tf(struct Trapframe *tf, int id);
extern void lcontext(u_int contxt);
:这两个函数是在env_asm.S
中定义的汇编函数,分别用来弹出栈中保存的上下文,以及切换上下文。void env_run(struct Env *e)
:调度一个新的进程。
最后梳理一下load_icode_mapper的思路,这个函数在lab6仍然要用到,因此最好一步到位,考虑到各种情况,不要写错,否则后患无穷。
|offset| |-----------|---BY2PG---|---......----|---BY2PG---|-----------|00000000000|000....000|00000000000| ^ ^ va va+bin_size
如上所示,例如加载时的首地址为va,当va不与一整页对齐时,offset为va%页的大小,load时先load一部分,让va与一页对齐,再以一整页为单位进行加载,最后如果剩余的部分仍不对齐,需要以byte为单位加载到对齐。除此之外需要注意的一点是,如果bin_size小于sg_size,需要将不足的部分填充0,这一部分为bss段,也就是未初始化的全局变量段,需要保证这一部分初值为0。
中断和异常
在计组课设的P7中已经接触到了中断和异常,当时十分强调的一点是“软硬件协同 ”,此处在操作系统课程设计中,关注的即是软件部分。在切换进程、进行IO等操作时均要使用系统调用,系统调用便是用户态进程使用内核态函数的方法,当系统陷入内核时需要依靠中断机制,CPU在硬件层面上实现了对中断机制的支持,操作系统则需要在软件层面上实现。
在发生中断和异常时,首先会跳转到一个固定的物理地址,这个地址是exception handler的首地址(这个跳转是由硬件保证的),在exception handler中对中断类型进行判断,并根据中断类型的不同跳转到不同的handler中进行中断处理。异常分发的代码为:
1 .section .text.exc_vec3 2 NESTED(except_vec3, 0, sp) 3 .set noat 4 .set noreorder 5 /* 6 * Register saving is delayed as long as we don't know 7 * which registers really need to be saved. 8 */ 9 1: 10 mfc0 k1,CP0_CAUSE 11 la k0,exception_handlers 12 /* 13 * Next lines assumes that the used CPU type has max. 14 * 32 different types of exceptions. We might use this 15 * to implement software exceptions in the future. 16 */ 17 18 andi k1,0x7c 19 addu k0,k1 20 lw k0,(k0) 21 NOP 22 jr k0 23 nop 24 END(except_vec3) 25 .set at
原理是取出异常描述码,并跳转到响应的handler处。为了绑定异常描述码与handler,需要异常向量组来实现。在traps.c中的以下部分代码便是为了这个目的:
1 void trap_init(){ 2 int i; 3 for(i=0;i<32;i++) 4 set_except_vector(i, handle_reserved); 5 set_except_vector(0, handle_int); 6 set_except_vector(1, handle_mod); 7 set_except_vector(2, handle_tlb); 8 set_except_vector(3, handle_tlb); 9 set_except_vector(8, handle_sys); 10 } 11 void *set_except_vector(int n, void * addr){ 12 unsigned long handler=(unsigned long)addr; 13 unsigned long old_handler=exception_handlers[n]; 14 exception_handlers[n]=handler; 15 return (void *)old_handler; 16 }
实现了将相应的handler的地址存入exception_handlers数组中的相应位置,在处理时直接跳转即可。
为了产生时钟中断,还需要开启时钟。开启时钟的代码是用汇编实现的,在调用时将其封装为了C函数进行调用。
1 #include <asm/regdef.h> 2 #include <asm/cp0regdef.h> 3 #include <asm/asm.h> 4 #include <kclock.h> 5 6 .macro setup_c0_status set clr 7 .set push 8 mfc0 t0, CP0_STATUS 9 or t0, \set|\clr 10 xor t0, \clr 11 mtc0 t0, CP0_STATUS 12 .set pop 13 .endm 14 15 .text 16 LEAF(set_timer) 17 18 li t0, 0x01 19 sb t0, 0xb5000100 20 sw sp, KERNEL_SP 21 setup_c0_status STATUS_CU0|0x1001 0 22 jr ra 23 24 nop 25 END(set_timer)
具体是将CP0的status寄存器的相应中断位置位,即可开启时钟。有了时钟中断,便可以实现时间片轮转算法。(不过在之后的lab里我的调度算法出现了一些问题,我直接改成了遍历整个内存控制块数组寻找可以调度的进程,直接将其调度的暴力法,比较摸鱼地摸过了后边几个lab…)