进程此时不仅是在运行而已,它可以随时被中断,可以在中断处理程序完成之后被恢复。进程此时已经有了两种状态:运行和睡眠。我们已经具备了处理多个进程的能力,只需要让其中一个进程处在运行态,其余进程处在睡眠态就可以了。
在main.c中进程A的代码的下面添加进程B:
void TestB()
{
int i = 0x1000;
while(1){
disp_str("B");
disp_int(i++);
disp_str(".");
delay(1);
}
}
打印的字母换成了B,i的初始值被设成了0x1000.
让我们来回忆一下当初准备第一个进程时还做了哪些工作。进程不外乎4个要素:进程表、进程体、GDT、TSS。
Minix系统中定义了一个数组叫做tasktab。这个数组的每一项定义好一个任务(“任何”和“进程”可以互换)的开始地址、堆栈等,在初始化的时候,只要用一个for循环依次读取每一项,然后填充到相应的进程表项中就可以了。
首先在proc.h中:
typedef struct s_task {
task_f initial_eip;
int stacksize;
char name[32];
}TASK;
一个进程只要有一个进程体和堆栈就可以运行了,所以这个数组只要有前两个成员其实就已经够了。这里我们还定义了name,以便给每个进程起一个名字。
PUBLIC TASK task_table[NR_TASKS] = {{TestA, STACK_SIZE_TESTA, "TestA"},
{TestB, STACK_SIZE_TESTB, "TestB"}};
别忘了在global.h中:
extern TASK task_table[];
记得把NR_TASKS的值修改为2.还有STACK_SIZE_TESTB:
/* Number of tasks */
#define NR_TASKS 2 /* stacks of tasks */
#define STACK_SIZE_TESTA 0x8000
#define STACK_SIZE_TESTB 0x8000 #define STACK_SIZE_TOTAL (STACK_SIZE_TESTA + \
STACK_SIZE_TESTB)
下面来做进程表的初始化工作。现在可以用for循环来做进程表的初始化工作了。
请看main.c的kernel_main()函数。在我们这个简单的例子中,进程之间区别真的不大。每一次循环的不同在于,从TASK结构中读取不同的任务入口地址、堆栈栈顶和进程名,然后赋给相应的进程表项。需要注意两点:
1.由于堆栈是从高地址往低地址生长的,所以在给每一个进程分配堆栈空间的时候也是从高地址往低地址进行。
2.我们为每一个进程都在GDT中分配一个描述符用来对应进程的LDT。我们在task_table中定义了几个任务,通过上文的for循环中的代码,GDT中就会有几个描述符被初始化,它们列在SELECTOR_LDT_FIRST之后。
此外,p_name和pid目前并没有什么实际的作用。
每一个进程都会在GDT中对应一个LDT描述符。可是ldt选择子仅仅是解决了where问题,通过它,我们能在GDT中找到相应的描述符,但描述符的具体内容是什么,what问题还没解决。
在protect.c中的init_prot函数也使用一个循环:
// 填充 GDT 中进程的 LDT 的描述符
int i;
PROCESS* p_proc = proc_table;
u16 selector_ldt = INDEX_LDT_FIRST << 3;
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;
}
另外,每个进程都有自己的LDT,所以当进程切换时需要重新加载ldtr,如下:
lldt [esp + P_LDT_SEL]
接下来修改中断处理程序来切换进程。
在任务切换过程中,首先,处理器中各寄存器的当前值被自动保存到TR(任务寄存器)所指定的TSS中;然后,下一任务的TSS的选择子被装入TR;最后,从TR所指定的TSS中取出各寄存器的值送到处理器的各寄存器中。
要想恢复不同的进程,只需要将esp指向不同的进程表就可以了,全局变量p_proc_ready是指向进程表结构的指针,我们只需要在下面这一句执行之前把它赋予不同的值就可以了。
mov esp, [p_proc_ready] ; 离开内核栈
我们再来学习一下Minix,创建一个clock.c。
PUBLIC void clock_handler(int irq)
{
disp_str("#");
}
在时钟中断例程中调用这个函数。
下面该进程切换了:
PUBLIC void clock_handler(int irq)
{
disp_str("#");
p_proc_ready++;
if (p_proc_ready >= proc_table + NR_TASKS)
p_proc_ready = proc_table;
}
在上面代码中,每一次我们让p_proc_ready指向进程表中的下一个表项,如果切换前已经到达进程表结尾则回到第一个表项。运行结果如下,我们看到了交替出现的“A”和“B”,还有各自不断增加的数字,实现了多进程:
【源码】