进程切换
本质:任务中断时将堆栈指针指向另外一个堆栈B,该堆栈中保存有B进程运行时,cpu 各类寄存器状态,当利用iretd返回时,CPU的寄存器值会被堆栈B中的状态填充,变成了进程B的状态,sp指针也指向任务B,此时任务跳到了B中,完成了任务切换
今天讲述的是一个比较简陋的进程切换,步骤如下
1.进程表初始化
2.调用restart()函数。
int kernel_main()
{
disp_str("-----\"kernel_main\" begins-----\n");
PROCESS* p_proc = proc_table;
p_proc->ldt_sel = SELECTOR_LDT_FIRST;
memcpy(&p_proc->ldts[0], &gdt[SELECTOR_KERNEL_CS>>3], sizeof(DESCRIPTOR));
p_proc->ldts[0].attr1 = DA_C | PRIVILEGE_TASK << 5; // change the DPL
memcpy(&p_proc->ldts[1], &gdt[SELECTOR_KERNEL_DS>>3], sizeof(DESCRIPTOR));
p_proc->ldts[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5; // change the DPL
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;
p_proc->regs.gs = (SELECTOR_KERNEL_GS & SA_RPL_MASK) | RPL_TASK;
p_proc->regs.eip= (u32)TestA;
p_proc->regs.esp= (u32) task_stack + STACK_SIZE_TOTAL;
p_proc->regs.eflags = 0x1202; // IF=1, IOPL=1, bit 2 is always 1.
p_proc_ready = proc_table;
restart();
while(1){}
}
可以看到 在kernel_main()前面大部分都是进程表结构体的初始化,接着调用restart如下,在下面程序中第四行、第五行的作用是将s_proc这个结构体中第一个结构体成员的末地址赋值给tss中ring0堆栈指针域esp.注意,这里是末地址,堆栈中已经存好值了,因为刚开始的时候,初始化便在里面存好了值。所以是末地址!!!
; ========================================================
; restart
; ==========================================================
restart:
mov esp, [p_proc_ready]
lldt [esp + P_LDT_SEL]
lea eax, [esp + P_STACKTOP]
mov dword [tss + TSS3_S_SP0], eax
pop gs
pop fs
pop es
pop ds
popad
add esp, 4
iretd
主要作用为 :将指向进程表的指针指向下一个进程表;加载局部描述符;将s_proc第一个结构体成员赋给TSS 中ring0堆栈指针,这个函数的作用是用来开始第一个跳转的时候使用的,在后期的跳转中,因为已经有了
在完成指针的转换操作后, 可以看到程序利用pop指令,将堆栈中保存的值都放到了寄存起中,然后通过iretd完成了任务切换(指针指向了任务B的esp)
在接下来的介绍中,我们需要用到中断,当中断到来,进行任务切换,这才是一般操作系统的用法,而不是运行到某函数,自然而然进行中断。
首先需要做的是实现一个中断,在前面我们看到有实现过定时器的中断,首先是打开时钟中断
void init_8259A(void)
{
/* Master 8259, ICW1. */
out_byte(INT_M_CTL, 0x11);
/* Slave 8259, ICW1. */
out_byte(INT_S_CTL, 0x11);
/* Master 8259, ICW2. 设置 '主8259' 的中断入口地址为 0x20. */
out_byte(INT_M_CTLMASK, INT_VECTOR_IRQ0);
/* Slave 8259, ICW2. 设置 '从8259' 的中断入口地址为 0x28 */
out_byte(INT_S_CTLMASK, INT_VECTOR_IRQ8);
/* Master 8259, ICW3. IR2 对应 '从8259'. */
out_byte(INT_M_CTLMASK, 0x4);
/* Slave 8259, ICW3. 对应 '主8259' 的 IR2. */
out_byte(INT_S_CTLMASK, 0x2);
/* Master 8259, ICW4. */
out_byte(INT_M_CTLMASK, 0x1);
/* Slave 8259, ICW4. */
out_byte(INT_S_CTLMASK, 0x1);
/* Master 8259, OCW1. */
out_byte(INT_M_CTLMASK, 0xFE);
/* Slave 8259, OCW1. */
out_byte(INT_S_CTLMASK, 0xFF);
}
接着是在中断处理函数中,我们需要自己的定制
ALIGN 16
hwint00:
pushad
push ds
push es
push fs
push gs
inc byte[gs:0] ; 改变屏幕第0行 第0列的字符 用作调试 证明进入了中断
mov al, EOI ;让时钟中断可以不断进入
out INT_M_CTL, al;
pop gs
pop fs
pop es
pop ds
popad
iretd ; 中断返回 装载堆栈内的数据
在中断中,为了防止内部的一些函数调用会改变 ds es fs gs 等等 ,因此用了压栈和出栈的操作用来保护这些寄存器的值 ,在这个中断处理函数的最后已经有了 出栈和iretd操作,可以让内核切换到新的任务中去
但是新的任务的压栈过程还没有写。
当前进程被中断切到内核态时,当前的各个就承诺期应该诶立即保存(压栈过程),也就是说,每个进程在运行时,tss.esp0应该是当前进程的进程表中保存寄存器值的地方,即struct s_proc中struct s_stackframe的最高地址处。这样进程被挂起后才恰好保存寄存器到正确的位置。我们假设进程A在运行,那么tss.esp0的值就是进程表A中regs的最高处,因为我们是不可能在进程A运行时来设置tss.esp0的值的,所以我们必须在A被恢复运行之前,即iretd执行之前做这件事。换句话说,我们需要在时钟中断处理函数结束之前做这件事。
这里我们需要的就不能是末地址了,而必须是堆栈的初地址。
设置tss.esp0
代码如下,其中新添加的
lea eax, [esp+P_STACKTOP]
mov dword[tss+TSS3_S_SP0], eax
便是为了设置tss.esp0, 此处添加的代码与restart中的代码一模一样,指向了堆栈底部。压栈压的是当前任务的栈,然后堆栈指针指向下一个任务的栈,出栈时就出的是下一个任务的栈,再用irted,任务就完全跳转到下一个任务了。
ALIGN 16
hwint00: ; Interrupt routine for irq 0 (the clock).
sub esp, 4
pushad
push ds
push es
push fs
push gs
mov dx, ss
mov ds, dx
mov es, dx
inc byte [gs:0]
mov al, EOI
out INT_M_CTL, al;
lea eax, [esp+P_STACKTOP]
mov dword[tss+TSS3_S_SP0], eax
pop gs
pop fs
pop es
pop ds
popad
add esp, 4
iretd ; 中断返回 装载堆栈内的数据
到目前为止,任务所能实现的状态为中断 任务A 中断 任务A 中断 任务A
还没有进行两个任务的中断。