进程的并行数取决于 CPU 的内核数量。以单核 CPU 为例,它在同一时间段只能执行一个进程。
由于 CPU 的进程调度太快,导致我们误以为它们都是同一时刻运行。
诱发进程切换的因素有很多,比较典型的是时间中断。
当时间中断发生时,中断处理程序会将控制权交给进程调度模块。
在同一时刻,只能有一个进程处于运行状态。
进程切换的操作者是操作系统的进程调度模块。
用一个数据结构来记录进程的状态信息:
进程和进程调度运行在不同的层级上,本书简单化了,使其所有任务都运行在 ring1,而进程切换运行在 ring0
进程切换时的步骤:
保存某个进程的状态信息: 入栈所有寄存器。
恢复某个进程的状态信息: 弹出所有寄存器。
进程表:是一个数组,由多个进程对象组成。
; 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
特权级变换:ring0 -> ring1
程序一开始我们的代码都是运行在 ring0 中,因此想要运行一个进程就需要从 ring0 -> ring1,这将是第一个运行的进程。
也就是说,一开始我们便可以假设成触发时钟中断,执行进程调度模块。
最简单的任务是:完成从 ring1 -> ring0
ALIGN 16
hwint00:
iretd
进程对象 PROCESS 中保存着进程的状态信息,一个进程若要运行,则需要依赖这里面的信息,因此我们需要初始化这些信息,使其成功运行第一个进程。
关系:
图示:
第一步:编写进程体。
// 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,然后弹出栈中各个值,从而执行进程
...
}
此时时钟中断只会发生一次,因为我们没有将中断结束位 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 描述符便不再是空壳。
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("#");
}
; 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);
}
}
声明 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);
}
}
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 个进程运行,便不会有误差。