当前位置: 首页 > news >正文

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

文章目录

    • 进程
    • 最简单的进程
      • 知识点
      • 代码实现
        • 时钟中断处理程序
        • 进程表、进程体、GDT、TSS 之间的关系
        • 启动进程
        • Tips
    • 多进程
      • 添加一个进程体
      • 定义相关变量与宏
      • 进程表的初始化
      • LDT
      • 时钟中断处理程序
      • 加载 LDTR
      • 效果一
      • 修改时钟中断处理程序 —— 进程切换
      • 效果二
      • 代码完成 —— 添加一个进程的步骤
      • 多进程代码优化
    • 系统调用
      • 最简单的系统调用
      • 图解系统调用
      • V2
      • get_ticks 的应用
        • 8253/8254 PIT
        • 代码实现
    • 进程调度

进程

  • 进程的并行数取决于 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:最大进程允许数

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

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

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

特权级变换:ring1 -> ring0

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

特权级变换:ring0 -> ring1

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

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

代码实现

时钟中断处理程序

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

ALIGN 16
hwint00:
	iretd

进程表、进程体、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);
    }
}
// kernel\main.c
PUBLIC int kernel_main() {
    disp_str("-----\"kernel_main\" begins-----\n");
    ...
    while(1);
}
; kernel\kernel.asm
extern kernel_main
...
cstart:
	jmp kernel_main

第二步:初始化进程表。

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);
}
// 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;
}
// include/protect.h
// 线性地址 -> 物理地址
#define vir2phys(seg_base, vir) (u32) (((u32) seg_base) + (u32) (vir))

第三步:准备 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);
    ...
}
; kernel/kernel.asm
csinit:
    ; 初始任务寄存器 TR
    xor     eax, eax
    mov     ax, SELECTOR_TSS
    ltr     ax
    
    jmp     kernel_main
// 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;

启动进程

; 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
// kernel/global.c
EXTERN PROCESS* p_proc_ready;

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

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

Tips

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

多进程

添加一个进程体

// kernel/main.c
void TestB() {
    int i = 0x1000;
    while(1) {
        disp_str("B");
        disp_int(i++);
        disp_str(".");
        delay(1);
    }
}

定义相关变量与宏

一个进程只要有一个进程体和堆栈就可以运行了,因为多个进程要同时运行,所以进程体和堆栈的位置管理变成了问题,这里我们定义一个数组 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;
// include/type.h
typedef void (*task_f) (); // 进程体的函数指针
// include/global.h
extern TASK         task_table[]; // 管理每个任务
// kernel/global.c
// 进程管理表
// 责任:记录一个任务(进程)的开始地址、堆栈等信息
PUBLIC TASK task_table[NR_TASKS] = {
                                        //进程体        堆栈         进程名
                                        {TestA, STACK_SIZE_TESTA, "TestA"}, 
                                        {TestB, STACK_SIZE_TESTB, "TestB"}
                                   };
// 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)

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

// include/proto.h
void TestB();

进程表的初始化

// 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);
}

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

补充:

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

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;
    }
}

时钟中断处理程序

// kernel/clock.c
PUBLIC void clock_handler(int req) {
    disp_str("#");
}

加载 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

效果一

参考 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;
}

效果二

参考 P206 - 图 6.18

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

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

// kernel/main.c
void TestC() {
    int i = 0x1000;
    while(1) {
        disp_str("C");
        disp_int(i++);
        disp_str(".");
        delay(1);
    }
}

第二步:在 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"}
                                   };

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

// 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)

第四步:添加函数声明。

// include/proto.h
void TestC();

多进程代码优化

%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

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

// 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); // 中断处理函数的函数指针

相关宏:

// 硬件中断
#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 */

初始化 irq_table:

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

单独对某个中断初始化:

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

系统调用

跳过 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

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

PUBLIC void init_prot() {
    init_idt_desc(INT_VECTOR_SYS_CALL,    DA_386IGate, sys_call,                PRIVILEGE_USER);
}

修改 save:

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

; 代码就不贴了...

编写 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

相关宏与变量:

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

编写系统函数:

// kernel/proc.c
PUBLIC int sys_get_ticks() {
    disp_str("+");
    return 0;
}

函数声明:

// include/proto.h
// 系统调用相关
// proc.c
PUBLIC int sys_get_ticks(); // sys_call

// syscall.asm
PUBLIC void sys_call(); // int_handler
PUBLIC int get_ticks();

修改进程体A:

// kernel/main.c
void TestA() {
    int i = 0;
    while(1) {
        get_ticks();
        disp_str("A");
        disp_int(i++);
        disp_str(".");
        delay(1);
    }
}

图解系统调用

在这里插入图片描述

V2

声明 ticks:

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

初始化 ticks:

PUBLIC int kernel_main() {
    ...
    ticks = 0;
    ...
}

修改时钟中断处理程序:

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;
}

修改系统处理函数:

// kernel/proc.c
PUBLIC int sys_get_ticks() {
    return ticks;
}

修改进程体A:

// kernel/main.c
void TestA() {
    int i = 0;
    while(1) {
        disp_str("A");
        disp_int(get_ticks(););
        disp_str(".");
        delay(1);
    }
}

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 上可软件设置)

设置 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));
}

编写延迟函数:

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

先得到 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);
    }
}

此时若其它进程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));
}

编写延迟函数:

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

先得到 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);
    }
}

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

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

相关文章:

  • Spring Cloud 拉取 Nacos 中配置文件
  • python-中断time.sleep一种更优雅的办法:event.wait
  • 【毕业设计】大数据公交数据分析与可视化 - 大数据 python falsk
  • Hadoop与Spark中的Shuffle过程梳理
  • CH9101芯片应用—硬件设计指南
  • [NCTF2019]True XML cookbook
  • 湖仓一体电商项目(十二):编写写入DM层业务代码
  • 遥感生态指数(RSEI)——四个指数的计算
  • 9--RNN
  • JDBC的使用
  • 《Mycat分布式数据库架构》之数据切分实战
  • SpringBoot使用spring.config.import多种方式导入配置文件
  • 【框架】Spring Framework :SpringBoot
  • Linux内核之waitqueue机制
  • 前端面试:webpack整理
  • 【跃迁之路】【463天】刻意练习系列222(2018.05.14)
  • Go 语言编译器的 //go: 详解
  • JavaScript设计模式系列一:工厂模式
  • mysql_config not found
  • PHP面试之三:MySQL数据库
  • Python socket服务器端、客户端传送信息
  • react 代码优化(一) ——事件处理
  • react-native 安卓真机环境搭建
  • TypeScript实现数据结构(一)栈,队列,链表
  • 初识MongoDB分片
  • 创建一个Struts2项目maven 方式
  • 创建一种深思熟虑的文化
  • 翻译--Thinking in React
  • 更好理解的面向对象的Javascript 1 —— 动态类型和多态
  • 关于使用markdown的方法(引自CSDN教程)
  • 前端技术周刊 2019-02-11 Serverless
  • 如何利用MongoDB打造TOP榜小程序
  • 使用API自动生成工具优化前端工作流
  • 详解NodeJs流之一
  • 赢得Docker挑战最佳实践
  • 用element的upload组件实现多图片上传和压缩
  • 字符串匹配基础上
  • C# - 为值类型重定义相等性
  • 阿里云移动端播放器高级功能介绍
  • ​DB-Engines 11月数据库排名:PostgreSQL坐稳同期涨幅榜冠军宝座
  • ​一些不规范的GTID使用场景
  • ​中南建设2022年半年报“韧”字当头,经营性现金流持续为正​
  • %3cscript放入php,跟bWAPP学WEB安全(PHP代码)--XSS跨站脚本攻击
  • (a /b)*c的值
  • (附源码)ssm码农论坛 毕业设计 231126
  • (附源码)计算机毕业设计大学生兼职系统
  • (七)Knockout 创建自定义绑定
  • (亲测有效)解决windows11无法使用1500000波特率的问题
  • (原)Matlab的svmtrain和svmclassify
  • (转载)OpenStack Hacker养成指南
  • ... fatal error LINK1120:1个无法解析的外部命令 的解决办法
  • ./和../以及/和~之间的区别
  • .babyk勒索病毒解析:恶意更新如何威胁您的数据安全
  • .MSSQLSERVER 导入导出 命令集--堪称经典,值得借鉴!
  • .NET Core WebAPI中使用swagger版本控制,添加注释