• 《Orange‘s 一个操作系统的实现》第六章


    进程

    • 进程的并行数取决于 CPU 的内核数量。以单核 CPU 为例,它在同一时间段只能执行一个进程。

    • 由于 CPU 的进程调度太快,导致我们误以为它们都是同一时刻运行。

    • 诱发进程切换的因素有很多,比较典型的是时间中断。

      当时间中断发生时,中断处理程序会将控制权交给进程调度模块。

    • 在同一时刻,只能有一个进程处于运行状态。

    • 进程切换的操作者是操作系统的进程调度模块。

    用一个数据结构来记录进程的状态信息:

    进程和进程调度运行在不同的层级上,本书简单化了,使其所有任务都运行在 ring1,而进程切换运行在 ring0

    最简单的进程

    知识点

    进程切换时的步骤:

    1. 进程 A 运行中。
    2. 时钟中断发生,从 ring1 -> ring0,时钟中断处理程序启动。
    3. 开始执行进程调度程序,指定好下一个要运行的进程(假设为进程 B)。
    4. 进程 B 从等待状态恢复到执行状态,从 ring0 -> ring1。
    5. 进程 B 开始运行。

    在这里插入图片描述

    保存某个进程的状态信息: 入栈所有寄存器。

    恢复某个进程的状态信息: 弹出所有寄存器。

    进程表:是一个数组,由多个进程对象组成。

    ; PROCESS 进程对象 —— 描述进程信息 include/proc.h
    typedef struct s_stackframe {
    	u32	gs;		    /* \                                    */
    	u32	fs;		    /* |                                    */
    	u32	es;		    /* |                                    */
    	u32	ds;		    /* |                                    */
    	u32	edi;		/* |                                    */
    	u32	esi;		/* | pushed by save()                   */
    	u32	ebp;		/* |                                    */
    	u32	kernel_esp;	/* <- 'popad' will ignore it            */
    	u32	ebx;		/* |                                    */
    	u32	edx;		/* |                                    */
    	u32	ecx;		/* |                                    */
    	u32	eax;		/* /                                    */
    	u32	retaddr;	/* return addr for kernel.asm::save()   */
    	u32	eip;		/* \                                    */
    	u32	cs;		    /* |                                    */
    	u32	eflags;		/* | pushed by CPU during interrupt     */
    	u32	esp;		/* |                                    */
    	u32	ss;		    /* /                                    */
    } STACK_FRAME;
    
    ; include/proc.h
    typedef struct s_proc {
    	STACK_FRAME regs;          // 进程的所有寄存器都保存在 STACK_FRAME 结构中
    
    	u16 ldt_sel;               // LDT Selector
    	DESCRIPTOR ldts[LDT_SIZE]; // 局部描述符 LDT
    	u32 pid;                   // 进程ID
    	char p_name[16];           // 进程名
    } PROCESS;
    
    ; 进程表 include\global.c
    PUBLIC PROCESS proc_table[NR_TASKS]
    ; NR_TASKS:最大进程允许数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    程序不同状态下 esp 指向不同的地方:

    • 进程栈 —— 进程运行时自身的堆栈。
    • 进程表 —— 存储进程状态信息的数据结构。
    • 内核栈 —— 进程调度模块运行时使用的堆栈。

    注:编码时切记要清楚当前使用的是哪个堆栈,以免破坏掉不应破坏的数据。

    特权级变换:ring1 -> ring0

    • 准备 TSS:特权级由外向内层转移时,需要从 TSS 中取出内层的 ss 和 esp 作为目标代码的 ss 和 esp。
    • 为每个进程准备 LDT:由于每个进程都是独立的,因此需要用到的描述符都要放在局部描述符表 LDT 中。

    特权级变换:ring0 -> ring1

    程序一开始我们的代码都是运行在 ring0 中,因此想要运行一个进程就需要从 ring0 -> ring1,这将是第一个运行的进程。

    也就是说,一开始我们便可以假设成触发时钟中断,执行进程调度模块。

    代码实现

    时钟中断处理程序

    最简单的任务是:完成从 ring1 -> ring0

    ALIGN 16
    hwint00:
    	iretd
    
    • 1
    • 2
    • 3
    进程表、进程体、GDT、TSS 之间的关系

    进程对象 PROCESS 中保存着进程的状态信息,一个进程若要运行,则需要依赖这里面的信息,因此我们需要初始化这些信息,使其成功运行第一个进程。

    关系:

    1. 进程对象和 GDT:进程对象中的 LDT Selector 对应 GDT 中的一个描述符,而这个描述符所指向的内存空间就存在于进程表内。
    2. 进程对象和进程:若一个进程发生了时钟中断,则各个寄存器的值都会被保存在进程对象中。程序初始时的第一个进程只需要初始化入口地址就好了。由于堆栈不受程序本身控制,因此需要先手动指定 esp 的值。
    3. GDT 和 TSS:GDT 中需要有一个描述符来对应 TSS,需要事先初始化这个描述符。

    图示:

    在这里插入图片描述

    第一步:编写进程体。

    // kernel\main.c
    void TestA() {
        int i = 0;
        while(1) {
            disp_str("A");
            disp_int(i++);
            disp_str(".");
            delay(1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    // kernel\main.c
    PUBLIC int kernel_main() {
        disp_str("-----\"kernel_main\" begins-----\n");
        ...
        while(1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    ; kernel\kernel.asm
    extern kernel_main
    ...
    cstart:
    	jmp kernel_main
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第二步:初始化进程表。

    STACK_FRAME、PROCESS 结构定义上文已经给出。

    初始化进程对象:

    // kernel\main.c
    PUBLIC int kernel_main() {
        disp_str("-----\"kernel_main\" begins-----\n");
    
        PROCESS* p_proc = proc_table; // 进程表
    
        p_proc -> ldt_sel = SELECTOR_LDT_FIRST; // 设置 LDT Selector
    
        // 将 SELECTOR_KERNEL_CS 所指向的描述符拷贝到进程 PCB 的 LDTS[0] 处
        memcpy(&p_proc -> ldts[0], &gdt[SELECTOR_KERNEL_CS >> 3], sizeof(DESCRIPTOR));
        p_proc -> ldts[0].attr1 = DA_C | PRIVILEGE_TASK << 5; // 设置属性,更改 DPL
        // 将 SELECTOR_KERNEL_DS 所指向的描述符拷贝到进程 PCB 的 LDTS[1] 处
        memcpy(&p_proc -> ldts[1], &gdt[SELECTOR_KERNEL_DS >> 3], sizeof(DESCRIPTOR));
        p_proc -> ldts[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5; // 设置属性,更改 DPL
        // Tips:右移 3 位表示去掉选择子后面的 TI 和 RPL,留下描述符索引
    
        // 构建选择子,选择子结构:描述符索引(15~3) TI(2) RPL(1~0)
        // LDT 共有两个描述符,分别被初始化成内核代码段和内核数据段,只是改变了一下 DPL 使其运行在低特权级下
    
        // CS 指向第一个描述符
        p_proc -> regs.cs = (0 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
        // 其它的指向第二个描述符
        p_proc -> regs.ds = (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
        p_proc -> regs.es = (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
        p_proc -> regs.fs = (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
        p_proc -> regs.ss = (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
        // gs 仍然指向显存,只是改变了 RPL
        p_proc -> regs.gs = (SELECTOR_KERNEL_GS & SA_RPL_MASK) | RPL_TASK;
        // TestA 这是最先开始运行的
        p_proc -> regs.eip = (u32) TestA;
        // esp 表示 TestA 这个程序运行所需的栈
        p_proc -> regs.esp = (u32) task_stack + STACK_SIZE_TOTAL;
        // 设置标志位,IF=1, IOPL=1, 第二位总是为 1
        // 设置 IOPL 后进程就可以使用 I/O 指令了
        // 并且中断会在 iretd 执行时被打开(之前在 kernel.asm 中 sti 被注释掉了,这里会自动打开)
        p_proc -> regs.eflags = 0x1202;
    
        // 挖坑
        
        while(1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    // kernel/protect.c
    PUBLIC void init_prot() {
    	...
        // 填充 GDT 中进程的 LDT 的描述符
        init_descriptor(&gdt[INDEX_LDT_FIRST], vir2phys(seg2phys(SELECTOR_KERNEL_DS), proc_table[0].ldts), LDT_SIZE * sizeof(DESCRIPTOR) - 1, DA_LDT);
    }
    
    // 根据段名(即段选择子)转32位段基地址
    PUBLIC u32 seg2phys(u16 seg) {
        // 根据描述符索引去 GDT 寻找对应的描述符
        DESCRIPTOR* p_dest = &gdt[seg >> 3]; 
        // 将描述符中分开的三个基地址拼凑成一个完整的段基地址
        return (p_dest -> base_high << 24 | p_dest -> base_mid << 16 | p_dest -> base_low);
    }
    
    // 初始化段描述符
    PRIVATE void init_descriptor(DESCRIPTOR* p_desc, u32 base, u32 limit, u16 attribute) {
        p_desc -> limit_low         = limit & 0x0FFFF;
        p_desc -> base_low          = base & 0x0FFFF;
        p_desc -> base_mid          = (base >> 16) & 0x0FF;
        p_desc -> attr1             = attribute & 0xFF;
        p_desc -> limit_high_attr2  = ((limit >> 16) & 0x0F) | (attribute >> 8) & 0xF0;
        p_desc -> base_high         = (base >> 24) & 0x0FF;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    // include/protect.h
    // 线性地址 -> 物理地址
    #define vir2phys(seg_base, vir) (u32) (((u32) seg_base) + (u32) (vir))
    
    • 1
    • 2
    • 3

    第三步:准备 GDT 和 TSS。

    // kernel/protect.c
    PUBLIC void init_prot() {
    	...
        // 准备 GDT 和 TSS
        memset(&tss, 0, sizeof(tss));
        tss.ss0 = SELECTOR_KERNEL_DS;
        init_descriptor(&gdt[INDEX_TSS], vir2phys(seg2phys(SELECTOR_KERNEL_DS), &tss), sizeof(tss) - 1, DA_386TSS);
        tss.iobase = sizeof(tss);
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    ; kernel/kernel.asm
    csinit:
        ; 初始任务寄存器 TR
        xor     eax, eax
        mov     ax, SELECTOR_TSS
        ltr     ax
        
        jmp     kernel_main
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    // kernel/protect.c
    typedef struct s_tss {
    	u32	backlink;
    	u32	esp0;	/* stack pointer to use during interrupt */
    	u32	ss0;	/*   "   segment  "  "    "        "     */
    	u32	esp1;
    	u32	ss1;
    	u32	esp2;
    	u32	ss2;
    	u32	cr3;
    	u32	eip;
    	u32	flags;
    	u32	eax;
    	u32	ecx;
    	u32	edx;
    	u32	ebx;
    	u32	esp;
    	u32	ebp;
    	u32	esi;
    	u32	edi;
    	u32	es;
    	u32	cs;
    	u32	ss;
    	u32	ds;
    	u32	fs;
    	u32	gs;
    	u32	ldt;
    	u16	trap;
    	u16	iobase;	/* I/O位图基址大于或等于TSS段界限,就表示没有I/O许可位图 */
    }TSS;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    启动进程
    ; kernel/kernel.asm
    extern p_proc_ready
    global restart
    
    ...
    
    restart:
    	mov	    esp, [p_proc_ready]             ; esp <- 进程(PROCESS)指针
    	lldt	[esp + P_LDT_SEL]               ; esp + P_LDT_SEL 指向 PROCESS.ldt_sel
        ; 下面两行:将进程对象中 regs 的末地址赋给 TSS 中 ring0 堆栈指针域(内核堆栈) esp0
    	lea	    eax, [esp + P_STACKTOP]         ; esp + P_STACKTOP 指向 PROCESS.regs 中的末地址,即栈顶
    	mov	    dword [tss + TSS3_S_SP0], eax   ; tss + TSS3_S_SP0 指向 TSS.esp0,
    
    	pop	    gs
    	pop	    fs
    	pop	    es
    	pop	    ds
    	popad
    
    	add	esp, 4
    
    	iretd
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    // kernel/global.c
    EXTERN PROCESS* p_proc_ready;
    
    • 1
    • 2

    进程对象已经初始化完毕,如今只需要让 esp 指向栈顶,然后各个值弹出即可。

    // kernel\main.c
    PUBLIC int kernel_main() {
        ...
        p_proc_ready = proc_table; // p_proc_ready 指向刚刚初始化完成的进程对象
        restart(); // 调用函数设置 esp,然后弹出栈中各个值,从而执行进程
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    Tips

    此时时钟中断只会发生一次,因为我们没有将中断结束位 EOI 置为 1,告知 8259A 当前中断结束。

    多进程

    添加一个进程体

    // kernel/main.c
    void TestB() {
        int i = 0x1000;
        while(1) {
            disp_str("B");
            disp_int(i++);
            disp_str(".");
            delay(1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    定义相关变量与宏

    一个进程只要有一个进程体和堆栈就可以运行了,因为多个进程要同时运行,所以进程体和堆栈的位置管理变成了问题,这里我们定义一个数组 task_table 来管理一个任务(即进程)的开始地址、堆栈等。

    // include/proc.h
    typedef struct s_proc {
    	STACK_FRAME regs;          // 进程的所有寄存器都保存在 STACK_FRAME 结构中
    	u16 ldt_sel;               // LDT Selector
    	DESCRIPTOR ldts[LDT_SIZE]; // 局部描述符 LDT
    	u32 pid;                   // 进程ID
    	char p_name[16];           // 进程名
    } PROCESS;
    
    typedef struct s_task {
        task_f      initial_eip;   // 进程体的函数指针
        int         stacksize;     // 该进程的堆栈大小
        char        name[32];      // 该进程的名称
    } TASK;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    // include/type.h
    typedef void (*task_f) (); // 进程体的函数指针
    
    • 1
    • 2
    // include/global.h
    extern TASK         task_table[]; // 管理每个任务
    
    • 1
    • 2
    // kernel/global.c
    // 进程管理表
    // 责任:记录一个任务(进程)的开始地址、堆栈等信息
    PUBLIC TASK task_table[NR_TASKS] = {
                                            //进程体        堆栈         进程名
                                            {TestA, STACK_SIZE_TESTA, "TestA"}, 
                                            {TestB, STACK_SIZE_TESTB, "TestB"}
                                       };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    // include/proc.h
    // 最大允许的进程数
    #define NR_TASKS	2
    
    // 进程中的栈
    #define STACK_SIZE_TESTA	0x8000
    #define STACK_SIZE_TESTB	0x8000
    
    #define STACK_SIZE_TOTAL	(STACK_SIZE_TESTA + STACK_SIZE_TESTB)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    最后也不要忘记在 proto.h 声明新的进程体:

    // include/proto.h
    void TestB();
    
    • 1
    • 2

    进程表的初始化

    // kernel/main.c
    PUBLIC int kernel_main() {
        disp_str("-----\"kernel_main\" begins-----\n");
    
        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;
    	
        /* 
           【1】SELECTOR_LDT_FIRST 是 GDT 中被定义的唯一一个描述符
           在 task_table 中定义的几个任务,便通过 for 初始化几个描述符,并且放在 SELECTOR_LDT_FIRST 之后
        */
        for(i = 0; i < NR_TASKS; i++) {
            strcpy(p_proc -> p_name, p_task -> name);
            p_proc -> pid = i;
    
            p_proc -> ldt_sel = selector_ldt; // 设置 LDT Selector
    
            // 将 SELECTOR_KERNEL_CS 所指向的描述符拷贝到进程 PCB 的 LDTS[0] 处
            memcpy(&p_proc -> ldts[0], &gdt[SELECTOR_KERNEL_CS >> 3], sizeof(DESCRIPTOR));
            p_proc -> ldts[0].attr1 = DA_C | PRIVILEGE_TASK << 5; // 设置属性,更改 DPL
            // 将 SELECTOR_KERNEL_DS 所指向的描述符拷贝到进程 PCB 的 LDTS[1] 处
            memcpy(&p_proc -> ldts[1], &gdt[SELECTOR_KERNEL_DS >> 3], sizeof(DESCRIPTOR));
            p_proc -> ldts[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5; // 设置属性,更改 DPL
            // Tips:右移 3 位表示去掉选择子后面的 TI 和 RPL,留下描述符索引
    
            // 构建选择子,选择子结构:描述符索引(15~3) TI(2) RPL(1~0)
            // LDT 共有两个描述符,分别被初始化成内核代码段和内核数据段,只是改变了一下 DPL 使其运行在低特权级下
    
            // CS 指向第一个描述符
            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;
            // gs 仍然指向显存,只是改变了 RPL
            p_proc -> regs.gs = (SELECTOR_KERNEL_GS & SA_RPL_MASK) | RPL_TASK;
            
            // 设置进程体(函数指针)的位置
            p_proc -> regs.eip = (u32) p_task -> initial_eip;
            /**
            	【2】由于堆栈是从高至低地址生长的,所以给每个进程分配空间时也要从高至低地址进行
             */
            p_proc -> regs.esp = (u32) p_task_stack; // esp 表示这个程序运行所需的栈
            // 设置标志位,IF=1, IOPL=1, 第二位总是为 1
            // 设置 IOPL 后进程就可以使用 I/O 指令了
            // 并且中断会在 iretd 执行时被打开(之前在 kernel.asm 中 sti 被注释掉了,这里会自动打开)
            p_proc -> regs.eflags = 0x1202;
    
            p_task_stack -= p_task -> stacksize; // 完成一个进程的堆栈空间分配后,需要减去之前分配的空间
            p_proc++; // 指向下一个进程
            p_task++; // 指向下一个任务信息
            selector_ldt += 1 << 3;
        }
    
        k_reenter = -1; // 判断是否中断嵌套的全局变量
    
        p_proc_ready = proc_table;
        restart();
    
        while(1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    每个进程都会在 GDT 中对应一个 LDT 描述符。但 p_proc -> ldt_sel 只是解决了描述符在哪儿的问题,但描述符里面的内容是什么却不知道。

    补充:

    // kernel/global.c
    PUBLIC char task_stack[STACK_SIZE_TOTAL]; // 所有进程堆栈的大小总和
    
    • 1
    • 2

    LDT

    初始化 LDT,完成后 LDT 描述符便不再是空壳。

    PUBLIC void init_prot() {
        ...
            
        int i;
        PROCESS* p_proc = proc_table;
        u16 selector_ldt = INDEX_LDT_FIRST << 3;
    
        // 填充 GDT 中进程的 LDT 的描述符
        for(i = 0; i < NR_TASKS; i++) {
            init_descriptor(&gdt[selector_ldt >> 3], 
                            vir2phys(seg2phys(SELECTOR_KERNEL_DS), proc_table[i].ldts), 
                            LDT_SIZE * sizeof(DESCRIPTOR) - 1, 
                            DA_LDT);
            p_proc++;
            selector_ldt += 1 << 3;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    时钟中断处理程序

    // kernel/clock.c
    PUBLIC void clock_handler(int req) {
        disp_str("#");
    }
    
    • 1
    • 2
    • 3
    • 4

    加载 LDTR

    ; kernel/kernel.asm
    ALIGN   16
    hwint00:                ; Interrupt routine for irq 0 (the clock).
            sub     esp, 4  ; 跳过 retaddr
            pushad
            push    ds
            push    es
            push    fs
            push    gs
            mov     dx, ss
            mov     ds, dx
            mov     es, dx
    
            inc     byte [gs:0] ; 改变第 0 行,第 0 列的字符
    
            mov     al, EOI       ;
            out     INT_M_CTL, al ; 设置 EOI 位 
    
            inc     dword [k_reenter]
            cmp     dword [k_reenter], 0
            jne     .re_enter ; 若发生中断重入,则跳入 .re_enter
    
            mov     esp, StackTop ; 切换到内核栈
    
            sti ; 再次开启中断
            push    0
            call    clock_handler ; 中断处理程序
            add     esp, 4
            cli ; 关闭中断
    
            mov     esp, [p_proc_ready] ; 离开内核栈
            lldt    [esp + P_LDT_SEL]
            lea     eax, [esp + P_STACKTOP]
            mov     dword [tss + TSS3_S_SP0], eax
    .re_enter:
            dec     dword [k_reenter]
            pop     gs
            pop     fs
            pop     es
            pop     ds
            popad
            add     esp, 4 ; 跳过 RETADR
    
            iretd
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    效果一

    参考 P205 - 图 6.17

    此时还未实现切换进程。

    修改时钟中断处理程序 —— 进程切换

    // kernel/clock.c
    PUBLIC void clock_handler(int req) {
        disp_str("#");
        p_proc_ready++; // 下一个进程
        // 若达到最大进程数,则重头开始
        if(p_proc_ready >= proc_table + NR_TASKS)
                p_proc_ready = proc_table;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    效果二

    参考 P206 - 图 6.18

    代码完成 —— 添加一个进程的步骤

    第一步:添加一个进程体。

    // kernel/main.c
    void TestC() {
        int i = 0x1000;
        while(1) {
            disp_str("C");
            disp_int(i++);
            disp_str(".");
            delay(1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    第二步:在 task_table 中添加进程信息。

    // kernel/global.c
    // 进程管理表
    // 责任:记录一个任务(进程)的开始地址、堆栈等信息
    PUBLIC TASK task_table[NR_TASKS] = {
                                            //进程体    堆栈          进程名
                                            {TestA, STACK_SIZE_TESTA, "TestA"}, 
                                            {TestB, STACK_SIZE_TESTB, "TestB"},
                                            {TestC, STACK_SIZE_TESTC, "TestC"}
                                       };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    第三步:修改相关的宏与变量。

    // include/proc.h
    // 最大允许的进程数
    #define NR_TASKS	3
    
    // 进程中的栈
    #define STACK_SIZE_TESTA	0x8000
    #define STACK_SIZE_TESTB	0x8000
    #define STACK_SIZE_TESTC	0x8000
    
    #define STACK_SIZE_TOTAL	(STACK_SIZE_TESTA + STACK_SIZE_TESTB + STACK_SIZE_TESTC)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    第四步:添加函数声明。

    // include/proto.h
    void TestC();
    
    • 1
    • 2

    多进程代码优化

    %macro hwint_master     1
            call    save
    
            in      al, INT_M_CTLMASK ;\
            or      al, (1 << %1)     ; | 不允许发生时钟中断
            out     INT_M_CTLMASK, al ;/
    
            mov     al, EOI         ;
            out     INT_M_CTL, al   ; 重置 EOI 位,告知中断结束
    
            sti ; 开启中断,CPU在响应中断的过程中会自动关闭中断,在这里重新开启,便可允许响应新的中断
            push    %1
            call    [irq_table + 4 * %1] ; 中断处理程序
            pop     ecx
            cli ; 关闭中断
    
            in      al, INT_M_CTLMASK ;\
            or      al, ~(1 << %1)    ; | 允许发生时钟中断
            out     INT_M_CTLMASK, al ;/
    
            ret 
    %endmacro
    
    ALIGN   16
    hwint00:                ; Interrupt routine for irq 0 (the clock).
            hwint_master    0
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    中断处理函数的定义与声明:

    // kernel/global.c
    PUBLIC irq_handler irq_table[NR_IRQ]; // 存储所有中断所对应的中断处理函数
    // include/global.h
    extern irq_handler irq_table[];
    // include/type.h
    typedef void (*irq_handler) (int irq); // 中断处理函数的函数指针
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    相关宏:

    // 硬件中断
    #define	NR_IRQ		    16	// IRQ 数量
    #define	CLOCK_IRQ	    0
    #define	KEYBOARD_IRQ	1
    #define	CASCADE_IRQ	    2	// 
    #define	ETHER_IRQ	    3	// 默认以太网中断向量
    #define	SECONDARY_IRQ	3	// 端口 2 的 RS232 中断向量
    #define	RS232_IRQ	    4	// 端口 1 的 RS232 中断向量
    #define	XT_WINI_IRQ	    5	/* xt winchester */
    #define	FLOPPY_IRQ	    6	// 软盘
    #define	PRINTER_IRQ	    7
    #define	AT_WINI_IRQ	    14	/* at winchester */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    初始化 irq_table:

    // kernel/main.c
    PUBLIC void init_8259A() {
        ...
        int i;
        // 默认全部中断都同一处理方式
        for(i = 0; i < NR_IRQ; i++) 
                irq_table[i] = spurious_irq;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    单独对某个中断初始化:

    // kernel/main.c
    // 对某个中断进行单独处理
    PUBLIC void put_irq_handler(int irq, irq_handler handler) {
        disable_irq(irq);
        irq_table[irq] = handler;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    系统调用

    跳过 int xxx 的形式进行调用。

    最简单的系统调用

    kernel/syscall.asm:

    
    %include "sconst.inc"
    
    _NR_get_ticks       equ 0    ; 要跟 global.c 中的 sys_call_table 的定义相对应
    INT_VECTOR_SYS_CALL equ 0x90 ; 中断类型码
    
    global get_ticks
    
    bits 32
    
    [section .text]
    
    get_ticks:
        mov     eax, _NR_get_ticks
        int     INT_VECTOR_SYS_CALL
        ret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    初始化系统调用的中断门:

    PUBLIC void init_prot() {
        init_idt_desc(INT_VECTOR_SYS_CALL,    DA_386IGate, sys_call,                PRIVILEGE_USER);
    }
    
    • 1
    • 2
    • 3

    修改 save:

    get_ticks 中有一条 mov eax, _NR_get_ticks 语句,这是用于选择处理对应的处理函数的,但 save 中也用到了 eax,因此 save 中的 eax 需要变更为 esi,避免冲突。

    ; 代码就不贴了...
    
    • 1

    编写 sys_call —— 发生中断时所调用的:

    ; kernel/kernel.asm
    extern sys_call_table
    global sys_call
    
    ; ==========================================
    ; 该函数应该算是调用具体的系统函数的一个中转站吧...
    ; ==========================================
    sys_call:
        call    save
        
        sti
        call    [sys_call_table + eax * 4] ; 调用 sys_call_table[eax + 4] 函数
        mov     [esi + EAXREG - P_STACKBASE], eax ; 将 sys_call_table[eax] 的函数返回值放在进程表中 eax 的位置
        										  ; 以便进程 P 被恢复时 EAX 中放的是正确的返回值
        cli
        
        ret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    相关宏与变量:

    /* system call */
    #define NR_SYS_CALL     1 // 系统调用的函数个数
    
    • 1
    • 2
    // kernel/global.c
    // 保存所有系统调用的处理函数
    PUBLIC system_call sys_call_table[NR_SYS_CALL] = {sys_get_ticks};
    
    • 1
    • 2
    • 3
    // include/type.h
    typedef void* system_call; // 系统调用的处理函数
    
    • 1
    • 2

    编写系统函数:

    // kernel/proc.c
    PUBLIC int sys_get_ticks() {
        disp_str("+");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    函数声明:

    // include/proto.h
    // 系统调用相关
    // proc.c
    PUBLIC int sys_get_ticks(); // sys_call
    
    // syscall.asm
    PUBLIC void sys_call(); // int_handler
    PUBLIC int get_ticks();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    修改进程体A:

    // kernel/main.c
    void TestA() {
        int i = 0;
        while(1) {
            get_ticks();
            disp_str("A");
            disp_int(i++);
            disp_str(".");
            delay(1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    图解系统调用

    在这里插入图片描述

    V2

    声明 ticks:

    // include/global.h
    EXTERN int ticks; // 发生时钟中断的次数
    
    • 1
    • 2

    初始化 ticks:

    PUBLIC int kernel_main() {
        ...
        ticks = 0;
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    修改时钟中断处理程序:

    PUBLIC void clock_handler(int req) {
        disp_str("#");
        
        ticks++; // 每发生一个时钟中断,便 +1
    
        if(k_reenter != 0) {
            disp_str("!");
            return;
        }
    
        p_proc_ready++;
    
        if(p_proc_ready >= proc_table + NR_TASKS)
                p_proc_ready = proc_table;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    修改系统处理函数:

    // kernel/proc.c
    PUBLIC int sys_get_ticks() {
        return ticks;
    }
    
    • 1
    • 2
    • 3
    • 4

    修改进程体A:

    // kernel/main.c
    void TestA() {
        int i = 0;
        while(1) {
            disp_str("A");
            disp_int(get_ticks(););
            disp_str(".");
            delay(1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    get_ticks 的应用

    8253/8254 PIT

    xdm 看书 P227 去吧…,我懒了…

    代码实现

    相关宏:

    // include/const.h
    /* 8253/8254 PIT (可编程时钟定时器) */
    #define TIMER0         0x40 // 定时器通道 0 的 I/O 端口
    #define TIMER_MODE     0x43 // 定时器模式控制的 I/O 端口
    #define RATE_GENERATOR 0x34 /* 00-11-010-0 :
                                 * Counter0 - LSB then MSB - rate generator - binary
                                 */
    #define TIMER_FREQ     1193182L // PC 和 AT 定时器的时钟频率
    #define HZ             100  // 时钟频率(IBM-PC 上可软件设置)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    设置 8253:

    // kernel/main.c
    PUBLIC int kernel_main() {
        // 初始化 8253 PIT
        out_byte(TIMER_MODE, RATE_GENERATOR);
        out_byte(TIMER0, (u8) (TIMER_FREQ/HZ));
        out_byte(TIMER0, (u8) ((TIMER_FREQ/HZ) >> 8));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    编写延迟函数:

    // kernel/clock.c
    PUBLIC void milli_delay(int milli_sec) {
        int t = get_ticks();
        while(((get_ticks() - t) * 1000 / HZ) < milli_sec);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    先得到 ticks 保存到 t,每次循环都获取一个 ticks,与开始时的 t 相减,得到的结果必然会以 10 进行递增,直到 < milli_sec 为止。
    可以这么理解:(此刻时间 - 过去时间) * 1000 / HZ

    修改进程体A:

    // kernel/main.c
    void TestA() {
        int i = 0;
        while(1) {
            disp_str("A");
            disp_int(get_ticks(););
            disp_str(".");
            milli_delay(1000);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    此时若其它进程B、C 也同上,处字母不同外,则运行时时钟中断发生频率可能会有误差。

    解决方案:尝试将 NR_TASKS = 1,设置 task_table[NR_TASKS] 此时便是 1 个进程运行,便不会有误差。

    进程调度

    代码简单,虽然知道代码如何运行,但总感觉我好像缺少了一些东西的理解…,罢了罢了,以后有机会再回头看看吧…

    #define HZ 100 // 时钟频率(IBM-PC 上可软件设置)

    
    **设置 8253:**
    
    ```c
    // kernel/main.c
    PUBLIC int kernel_main() {
        // 初始化 8253 PIT
        out_byte(TIMER_MODE, RATE_GENERATOR);
        out_byte(TIMER0, (u8) (TIMER_FREQ/HZ));
        out_byte(TIMER0, (u8) ((TIMER_FREQ/HZ) >> 8));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    编写延迟函数:

    // kernel/clock.c
    PUBLIC void milli_delay(int milli_sec) {
        int t = get_ticks();
        while(((get_ticks() - t) * 1000 / HZ) < milli_sec);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    先得到 ticks 保存到 t,每次循环都获取一个 ticks,与开始时的 t 相减,得到的结果必然会以 10 进行递增,直到 < milli_sec 为止。
    可以这么理解:(此刻时间 - 过去时间) * 1000 / HZ

    修改进程体A:

    // kernel/main.c
    void TestA() {
        int i = 0;
        while(1) {
            disp_str("A");
            disp_int(get_ticks(););
            disp_str(".");
            milli_delay(1000);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    此时若其它进程B、C 也同上,处字母不同外,则运行时时钟中断发生频率可能会有误差。

    解决方案:尝试将 NR_TASKS = 1,设置 task_table[NR_TASKS] 此时便是 1 个进程运行,便不会有误差。

  • 相关阅读:
    科学技术创新杂志科学技术创新杂志社科学技术创新编辑部2022年第24期目录
    毫秒时间戳转换为字符串
    非最小相位系统;频率特性的对称性
    全局光照RSM
    工程项目管理系统源码+spring cloud 系统管理+java 系统设置+二次开发
    HTML批量文件上传方案——图像预览方式
    “数字+”就业生态系统演进变迁机理研探
    Web网络基础
    xxl-job项目集成实战,全自动项目集成,可以直接使用到项目中
    STM32存储左右互搏 QSPI总线FATS文件读写FLASH W25QXX
  • 原文地址:https://blog.csdn.net/qq_43098197/article/details/126793475