武汉大学21级网安操作系统大作业 任务一
1. 问题介绍与思路摘要
1.1 问题介绍
在已有实验代码基础上,将1-8章节(可以包含第9章)进行功能综合,形成你自己的一个简易OS
功能要求:
? 可以考虑使用软盘或者硬盘,启动该mini-OS。
? 能够实现你在前面章节所实现的,内存分配与释放,
? 能够实现你在前面章节所实现的多进程管理与调度,
? 所有代码需用目录树结构管理,并添加完整的makefile编译,以及文档
1.2 思路摘要
根据老师点拨,法一决定在chapter10的基础上做,法二是在chapter6的基础上做的。
在本学期“内核雏形”一节的实验中,我们利用makefile初步形成了一个内核雏形,其中已经添加了在屏幕上画一个喜欢的ASCII图案、内存分配与释放、自己设计的键盘中断;在本学期“多进程与进程调度”一节的实验中,我们实现了多级反馈队列调度算法。
基于此,将之前完成的功能:启动代码,在引导过程中在屏幕上画一个喜欢的ASCII图案;内存分配与释放;自己设计的键盘中断;多级反馈队列调度算法等集成。
2. 具体思路及其实现(法一)
2.1 在屏幕上画一个喜欢的ASCII图案
在start.c中加入printself函数,内容自定义,如
PUBLIC void printDP() { /* 通过打印空格的方式清空屏幕的前17行,并把 disp_pos 清零 */ int i; disp_pos = 0; for(i=0;i<80*17;i++){ disp_str(" "); } disp_pos = 0; disp_str( "Welcom to my test! " //自己可以自定义打印内容 ); }
然后,在kernel.asm引入并调用
效果如下
2.2 内存分配与释放
之前的实验中,将函数写在了一个文件中,即page.asm文件,直接复制到mm文件夹下,这是其中较为重要的几个函数。
DispTransfer: ; 保存现有的寄存器状态 xchg bx,bx pusha ; 打印base_address call DispReturn push szBASE call DispStr add esp, 4 ; 打印虚拟地址 mov eax, VirtualAddr push eax call DispInt add esp, 4 ; 打印PDE_base_address call DispReturn push szPDE call DispStr add esp, 4 ; 计算并打印页目录项 ; mov ax, SelectorFlatRW ; mov es, ax mov eax, VirtualAddr shr eax, 22 cmp eax, [PageTableNumber] ; jg .error mov ebx, 4 mul ebx mov edx, cr3 add eax, edx mov ecx, dword [es:eax] xor ecx, PG_P | PG_USU |PG_RWW and ecx, 0xfffffc00 push dword [es:eax] call DispInt add esp, 4 ; 打印PDE_base_address call DispReturn push szPTE call DispStr add esp, 4 ; 计算打印页表项 mov eax, VirtualAddr shr eax, 12 and eax, 0x3FF mov ebx, 4 mul ebx add eax, ecx mov ecx, dword [es:eax] xor ecx, PG_P | PG_USU | PG_RWW and ecx, 0xfffff000 push dword [es:eax] call DispInt add esp, 4 ; 打印Final_address call DispReturn push szFINAL call DispStr add esp, 4 ;计算打印最终地址 mov eax, VirtualAddr and eax, 0xFFF add eax, ecx push eax call DispInt add esp, 4 ; 恢复寄存器并返回 popa ret .error: ; 打印错误消息 push szAddressNotPresent call DispStr add esp, 4 ; 恢复寄存器并返回 popa
alloc_pages: ;save register ; push ds ; push es ; ;function implement ; mov bx, SelectorFlatRW ; mov ds,bx ; mov bx,SelectorData ; mov es,bx mov ecx,eax ;计算总共需要分配的物理内存大小,并将其存储在 ebx 寄存器中。 mov ebx,4096 ;page number * page size mul ebx mov ebx,[es:AvaLinearAddress] add [es:AvaLinearAddress],eax ;ebx存的是AvaLinearAddress 指定的线性地址对应的地址 push ebx mov eax,ebx mov ebx,cr3 and ebx,0xfffff000 and eax,0xffc00000 shr eax,20 add ebx,eax ;ebx中是PDE中的地址 mov edx,ebx ;edx->missing PDE address页目录项(PDE)的地址 mov ebx,[ebx] ;PTE 地址 ;test p flag 检查 PDE 中的 P 标志位 test ebx,0x0000_0001 jnz .pde_exist ;page table is not exist mov ebx,cr3 mov ebx,[ebx] ;first page table address and ebx,0xfffff000 shl eax,10 add ebx,eax ;这里会直接变为页表的位置 or ebx,0x0000_0007 mov [edx],ebx .pde_exist: mov eax,[esp] and ebx,0xfffff000 and eax,0x003ff000 shr eax,10 add ebx,eax .change_pte: call alloc_a_4k_page test eax,eax jz .allocation_failed ;分配失败 or eax,0x0000_0007 mov [ebx],eax add ebx,4 loop .change_pte .allocation_failed: pop ebx ; pop es ; pop ds ret alloc_a_4k_page: ;return eax: physical address ;保存寄存器的值 push ds push es ;function implement,设置合适的段寄存器的值,以确保访问正确的内存段 xor eax,eax ; mov ax,SelectorFlatRW ; mov es,ax ; mov ax,SelectorData ; mov ds,ax .search: bts [BitMap],eax jnc .find inc eax cmp eax,BitMapLen*8 jl .search ;no available physical space,执行 hlt 指令来暂停 CPU 的执行,表示没有可用的物理空间。 ;hlt;在分页分配中,使用 hlt 来表示分配失败并暂停 CPU 的执行是一种不常见的方式。 mov eax,0 ret .find: shl eax,12 pop es pop ds ret
free_pages: ; push ds ; push es push ebx ; save eax and ebx push eax ; mov bx, SelectorFlatRW ; mov ds, bx ; mov bx, SelectorData ; mov es, bx ; normal init ; find the pde and pte mov ebx, cr3 and ebx, 0xfffff000 and eax, 0xffc00000 shr eax, 20 ; 20 = 22 -2 add ebx, eax mov edx, [ebx] and edx, 0xfffffff8 mov [ebx], edx mov ebx, [ebx] mov eax, [esp] add esp, 4 and ebx,0xfffff000 and eax,0x003ff000 shr eax,10 add ebx, eax mov ecx, [esp] add esp,4 .free_pte: mov eax, [ebx] and eax, 0xfffffff8 mov edx, eax ; eax:映射的物理地址 shr edx, 12 ;edx:第edx个页面(由于物理地址开始为0x00000000) btr [BitMap], edx ;改变位图,设置为0,空闲 mov [ebx], eax add ebx,32 loop .free_pte ; pop es ; pop ds ret
然后在 kernel.asm中引入需要的函数
再添加 AllocAndFree函数并声明、调用
AllocAndFree: xchg bx,bx mov eax,4 call alloc_pages xchg bx,bx mov eax,ebx mov ebx,4 call free_pages xchg bx,bx ret
最后再在Makefile中 添加相关指令
代码效果如下图
这是DispTransfer(打马赛克的区域是个人信息,见谅)
这是alloc free
2.3 自己设计的键盘中断
在hwint01中添加自己设计的键盘中断代码,下面是示例代码
ALIGN 16 hwint01: ; Interrupt routine for irq 1 (keyboard) inc byte [gs:((80 * 17 + 50) * 2)] in al, 0x60 movzx ecx, al ;mov ecx, 8 cmp ecx,50 jg iretd_label lea ebx,[array+ecx] mov al,[ebx] mov ah,0ch mov [gs:((80 * 17 + 40)*2)],ax iretd_label: mov al,20h out 20h,al iretd
然后在一些代码地方添加能引发键盘中断的指令,运行后即可得到下图
2.4 多级反馈队列调度算法(仅展示关键代码)
首先,在sys/proc.h中定义多级反馈队列的三个队列
在kernel/global.c中初始化多级反馈队列
然后,在include/sys/global.h中声明,作为全局可用,这样在任何地方都有queue指向第一个队列
然后,在include/sys/proc.h中为每个进程添加参数inqueue(是否在队列中)、runtime(当前剩余时间)、queuenum(所在队列号)等参数。
再在kernel/proc.c中将入队和出队封装成两个函数inqueue和outqueue
int inqueue(struct proc* p) { // 把p进队 QUEUE* tempqueue; tempqueue = queue + p->queuenum; // 将当前程序放到队尾 tempqueue->taskqueue[tempqueue->rear] = p; tempqueue->rear = (tempqueue->rear + 1) % QUEUE_LEN; p->inqueue = 1; tempqueue->len += 1;
void outqueue(struct proc* p) { // 把p所在队的队首出队 QUEUE* tempqueue; // __asm__ __volatile__("xchg %bx,%bx"); tempqueue = queue + p->queuenum; tempqueue->front = (tempqueue->front + 1) % QUEUE_LEN; p->inqueue = 0; tempqueue->len -= 1; // 队首就绪, 出队 }
接下来是重头戏,即多级队列调度算法
首先仿造原schedule中的方法, 判断是否有新进程入队, 若一个进程当前为可执行状态, 并且不在队中, 则将其入队. 因为每次执行进程都会使进程出队, 所以在最开始初始化保证了每次调用该函数都能拿到进程。
struct proc* get_one_proc() { // 每次能拿到一个进程 , 且不可能拿不到 struct proc* p; QUEUE* tempqueue; int now_queue_num = 0; for (p = &FIRST_PROC; p <= &LAST_PROC; p++) { if (p->p_flags == 0 && p->inqueue == 0 && (p != p_proc_ready || queue->len == 0)) { // __asm__ __volatile__("xchg %bx, %bx");makei e p->ticks = queue->timep; // 进队进行初始化 p->queuenum = 0; p->runtime = 100; inqueue(p); // disp_int(p - &FIRST_PROC); } } now_queue_num = 0; while (now_queue_num <= 2) { // 在三个队列中找能运行的程序 tempqueue = queue + now_queue_num; int length = 0; int point = tempqueue->front; while (tempqueue->taskqueue[point]->p_flags != 0 && length < tempqueue->len) { outqueue(tempqueue->taskqueue[point]); inqueue(tempqueue->taskqueue[point]); // 如果是阻塞的则先出队再入队 length += 1; point = (point + 1) % QUEUE_LEN; } if (length < tempqueue->len) { // 找到一个 return tempqueue->taskqueue[point]; } now_queue_num++; } }
然后编写调度函数, 需要考虑, 是否发生抢占? 若没发生, 当前时间片是否用完, 当前程序运行时间是否结束? 基于以上分支, 编写调度函数如下所示
void myschedule() { struct proc *p, *next; next = get_one_proc(); if (p_proc_ready->ticks && p_proc_ready->runtime && next->queuenum >= p_proc_ready->queuenum) { //当前进程有时间片和运行时间,且没有优先级更高的,继续 return; } if (p_proc_ready->runtime == 0) { // 运行时间用完,换下一个 p_proc_ready = next; outqueue(next); return; } if (p_proc_ready->ticks == 0) { // 当前时间片用完, 进队,且选下一个 if (p_proc_ready->queuenum < 2) { p_proc_ready->queuenum += 1; // 进到下一队列 } p_proc_ready->ticks = (queue + p_proc_ready->queuenum)->timep; inqueue(p_proc_ready); p_proc_ready = next; outqueue(next); return; } if (next->queuenum < p_proc_ready->queuenum) { // 抢占 有更高的优先级 inqueue(p_proc_ready); p_proc_ready = next; outqueue(next); return; } }
然后,在include/sys/proto.h声明myschdule,因为后面别的文件要调用。
此处还存在一个地方对进程的调度存在控制, 因为此处存储在进程的阻塞, 而程序阻塞后需要选出下一个执行的程序, 否则就会发生死锁, 所以需要在下面的block函数中也进行相应的修改。
PRIVATE void block(struct proc* p) { assert(p->p_flags); inqueue(p); struct proc* next = get_one_proc(); p_proc_ready = next; outqueue(next); // disp_int(p_proc_ready - &FIRST_PROC); }
最后,修改clock.c中的clock_handler函数让当前进程的时间片和运行时间减少。
PUBLIC void clock_handler(int irq) { if (++ticks >= MAX_TICKS) ticks = 0; if (key_pressed) inform_int(TASK_TTY); if (k_reenter != 0) { return; } // if (p_proc_ready->ticks > 0) { // return; // } if (p_proc_ready->ticks) p_proc_ready->ticks--; if (p_proc_ready->runtime) p_proc_ready->runtime--; myschedule(); __asm__ __volatile__("xchg %bx,%bx"); switch ((p_proc_ready - &FIRST_PROC)) { case 0x6: disp_str("A,"); break; case 0x7: disp_str("B,"); break; case 0x8: disp_str("C,"); break; default: break; } disp_str(""); }
编译运行后,效果如下图
但是,可以看到,存在问题
不知道为啥,这里一开始少了一个A。另外,达到5个之后,队列以C、A、B的顺序从5到8到9才到10。
2.5 Makefile集成
在目录下执行tree指令,得到如下目录树。Makefile中的修改不多,只有在内存与分配中修改了一小部分,详细可看对应小节,此处略去。
. ├── 80m.img ├── a.img ├── bochsrc ├── boot │ ├── boot.asm │ ├── boot.bin │ ├── include │ │ ├── fat12hdr.inc │ │ ├── load.inc │ │ └── pm.inc │ ├── loader.asm │ └── loader.bin ├── command │ ├── echo.c │ ├── Makefile │ ├── pwd.c │ └── start.asm ├── fs │ ├── disklog.c │ ├── link.c │ ├── main.c │ ├── misc.c │ ├── open.c │ └── read_write.c ├── include │ ├── stdio.h │ ├── string.h │ ├── sys │ │ ├── config.h │ │ ├── console.h │ │ ├── const.h │ │ ├── fs.h │ │ ├── global.h │ │ ├── hd.h │ │ ├── keyboard.h │ │ ├── keymap.h │ │ ├── proc.h │ │ ├── protect.h │ │ ├── proto.h │ │ ├── sconst.inc │ │ └── tty.h │ └── type.h ├── kernel │ ├── clock.c │ ├── console.c │ ├── global.c │ ├── hd.c │ ├── i8259.c │ ├── kernel.asm │ ├── keyboard.c │ ├── kliba.asm │ ├── klib.c │ ├── main.c │ ├── proc.c │ ├── protect.c │ ├── start.c │ ├── systask.c │ └── tty.c ├── kernel.bin ├── krnl.map ├── lib │ ├── close.c │ ├── exec.c │ ├── exit.c │ ├── fork.c │ ├── getpid.c │ ├── misc.c │ ├── open.c │ ├── orangescrt.a │ ├── printf.c │ ├── read.c │ ├── stat.c │ ├── string.asm │ ├── syscall.asm │ ├── syslog.c │ ├── unlink.c │ ├── vsprintf.c │ ├── wait.c │ └── write.c ├── Makefile ├── mm │ ├── exec.c │ ├── forkexit.c │ ├── main.c │ └── page.asm └── scripts ├── genlog └── splitgraphs 10 directories, 78 files
至此,任务一第一种方法完成。
3. 具体思路及其实现(法二)
在第六章代码基础上完成,前面的流程与法一都一样,只有多级队列调度部分的代码不一样。(仅展示重要函数)
先是在main.c中修改kernel_main函数
PUBLIC int kernel_main() { disp_str("-----"kernel_main" begins----- "); int j; disp_pos = 0; for(j=0;j<80*25;j++){ disp_str(" "); } disp_pos = 0; TASK* p_task = task_table; PROCESS* p_proc = proc_table; char* p_task_stack = task_stack + STACK_SIZE_TOTAL; u16 selector_ldt = SELECTOR_LDT_FIRST; int i; for (i = 0; i < NR_TASKS; i++) { strcpy(p_proc->p_name, p_task->name); // name of the process p_proc->pid = i; // pid p_proc->ldt_sel = selector_ldt; memcpy(&p_proc->ldts[0], &gdt[SELECTOR_KERNEL_CS >> 3], sizeof(DESCRIPTOR)); p_proc->ldts[0].attr1 = DA_C | PRIVILEGE_TASK << 5; memcpy(&p_proc->ldts[1], &gdt[SELECTOR_KERNEL_DS >> 3], sizeof(DESCRIPTOR)); p_proc->ldts[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5; p_proc->regs.cs = ((8 * 0) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; p_proc->regs.ds = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; p_proc->regs.es = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; p_proc->regs.fs = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; p_proc->regs.ss = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; p_proc->regs.gs = (SELECTOR_KERNEL_GS & SA_RPL_MASK) | RPL_TASK; p_proc->regs.eip = (u32)p_task->initial_eip; p_proc->regs.esp = (u32)p_task_stack; p_proc->regs.eflags = 0x1202; /* IF=1, IOPL=1 */ p_task_stack -= p_task->stacksize; p_proc++; p_task++; selector_ldt += 1 << 3; } time_slice = 5;//初始化 process_num = 0; // for ( i = 0; i < NR_TASKS; i++) // { // proc_table[i].priority = 1; // proc_table[i].ticks = time_slice; // } proc_table[0].ticks = 4; proc_table[0].priority = 1; proc_table[1].ticks = 8; proc_table[1].priority = 2; proc_table[2].ticks = 16; proc_table[2].priority = 3; k_reenter = 0; ticks = 0; p_proc_ready = proc_table; /* 初始化 8253 PIT */ out_byte(TIMER_MODE, RATE_GENERATOR); out_byte(TIMER0, (u8) (TIMER_FREQ/HZ) ); out_byte(TIMER0, (u8) ((TIMER_FREQ/HZ) >> 8)); put_irq_handler(CLOCK_IRQ, clock_handler); /* 设定时钟中断处理程序 */ enable_irq(CLOCK_IRQ); /* 让8259A可以接收时钟中断 */ restart(); while(1){} }
然后修改clock.c中的clock_handler函数
PUBLIC void clock_handler(int irq) { ticks++; p_proc_ready->ticks--; PROCESS* p; //disp_int(p_proc_ready->pid); //if (k_reenter != 0) { // return; //} if(p_proc_ready->priority == 1) { if(p_proc_ready->ticks == 0) { p_proc_ready->priority = 2; p_proc_ready->ticks = time_slice * 2; schedule(); } }else if(p_proc_ready->priority == 2){ if(p_proc_ready->ticks != 0) { for(p = proc_table; p < proc_table + NR_TASKS; p++) { if (p->priority == 1) { p_proc_ready = p; return; } } for(p = proc_table; p < proc_table + NR_TASKS; p++) { if (p->priority == 2) { p_proc_ready = p; return; } } }else{ p_proc_ready->priority = 3; p_proc_ready->ticks = time_slice * 3; schedule(); } }else if(p_proc_ready->priority == 3){ if(p_proc_ready->ticks != 0) { for(p = proc_table; p < proc_table + NR_TASKS; p++) { if (p->priority == 1) { p_proc_ready = p; return; } } for(p = proc_table; p < proc_table + NR_TASKS; p++) { if (p->priority == 2) { p_proc_ready = p; return; } } }else{ p_proc_ready->ticks = time_slice * 3; schedule(); if(p_proc_ready->priority == 3) { while(1){ process_num = (process_num + 1) % NR_TASKS; p_proc_ready = proc_table + process_num; if(p_proc_ready->priority == 3) break; } } } } return; //if (p_proc_ready->ticks > 0) { // return; //} //schedule(); }
还有schedule函数
PUBLIC void schedule() { PROCESS* p; int inf = 0xfffffff; int greatest_priority = 0xfffffff; while (greatest_priority == inf) { for (p = proc_table; p < proc_table+NR_TASKS; p++) { if (p->priority < greatest_priority) { greatest_priority = p->priority; p_proc_ready = p; } } // if (!greatest_ticks) { // for (p = proc_table; p < proc_table+NR_TASKS; p++) { // p->ticks = p->priority; // } // } } }
编译运行后,结果如下
可以看到,显示根据时间片和优先级输出,最后优先级一致后,则进行轮询,故实验成功。