操作系统大作业——基于OrangeOS的改写(1)

武汉大学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;  
        //  }  
        // }  
    }  
}  

编译运行后,结果如下 

 可以看到,显示根据时间片和优先级输出,最后优先级一致后,则进行轮询,故实验成功。