《ORANGE-s-一个操作系统实现》进程(4-1)

进程概述

进程介绍

系统中运行的若干进程可以类比成一个人在一天内要做的若干样工作:总体看来,每样工作相对独立,并可产生某种结果;从细节上看,每样工作都具有自己的办法、工具和需要的资源;从时间上看,每一个时刻只能有一项工作正在处理中,各项工作可以轮换来做,这对于最终结果没有影响。
进程类似,从宏观上看,它有自己的目标,或者说功能,同时又能受控于进程调度模块,从微观来看,她可以利用系统的资源,有自己的代码和数据,同时拥有自己的堆栈;进程需要被调度,就好比一个人轮换做不同的工作。

示意图如下:
mark

形成进程的必要考虑

因为进程数是多余CPU数的,于是在同一时刻,总是有“正在运行的”和“正在休息的”进程。所以,对于“正在休息的”进程,我们需要让它在重新醒来的时候记住自己挂起之前的状态,以便让原来的任务继续执行下去。
所以,我们要一个数据结构记录一个进程的状态,在进程要被挂起的时候,进程信息就被写入这个数据结构,等到进程重新启动的时候,这个信息重新被读出来。如下图:
mark

最简单的进程

我们设想,当一个进程运行的时候,突然发生了时钟中断,特权级从ring1跳到ring0,开始执行时钟中断处理程序,中断处理程序这时调用进程调度模块,指定下一个应该运行的程序,当中断处理程序结束时,下一个进程准备就绪并开始运行,特权级又从ring0跳回ring1。我们把这个过程按照时间顺序整理如下:

  1. 进程A运行中
  2. 时钟中断发生,ring1->ring0,时钟中断处理程序启动。
  3. 进程调度,下一个应该运行的进程B被指定
  4. 进程B被恢复,ring0->ring1
  5. 进程B运行中。
    而要想实现这些功能,我们必须完成的应该有以下几项:
  6. 时钟中断处理程序
  7. 进程调度模块
  8. 两个进程
    进程切换图:
    mark

    准备工作

    进程控制块PCB(也叫做进程表)

    它相当于进程的提纲,通过PCB我们可以很方便的进行进程管理。
    因为我们会有跟多个PCB所以会形成如图所示的进程表:
    mark

    进程栈和内核栈

    当寄存器的值已经被保存到进程表内,进程调度模块就开始执行了,寄存器被压到进程表之后,esp汁指向进程表某个位置的。为了避免错误的出现,一定要将esp指向专门的内核栈区域。这样在短短的进程切换过程中,esp的位置出现在3个不同的区域:
  • 进程栈:进程运行时自身的堆栈
  • 进程表:存储进程状态信息的数据结构
  • 内核栈:进程调度模块运行时使用的堆栈
    mark

    第一步:ring0->ring1

在开始第一个进程时,我们用iretd来实现由ring0->ring1的转移,一旦转移成功,便可以认为已经在一个进程中运行了。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
restart:
mov esp, [p_proc_ready]
lldt [esp + P_LDT_SEL]
lea eax, [esp + P_STACKTOP]
mov dword [tss + TSS3_S_SP0], eax
restart_reenter:
dec dword [k_reenter]
pop gs
pop fs
pop es
pop ds
popad
add esp, 4
iretd

其中,指针p_proc_ready是指向PCB的指针,PCB的信息被结构体s_proc存储。当要恢复一个进程时,便将esp指向这个结构体的开始处,然后运行一系列的pop命令,将寄存器值弹出。进程表的开始位置结构图如下:
mark

时钟中断处理程序

我们只完成最简单的ring0到ring1的转移,做到这一点用一个iretd指令就够了

PCB、进程体、GDT、TSS

既然在进程开始之前要用到进程表中各项的值,我们应该先将这些值进行初始化,只要制定好各段寄存器、eip、esp以及eflags,它就可以正常运行,至于其他寄存器是用不到的,所以我们得出这样的必须初始化的寄存器列表:cs、ds、es、fs、gs、ss、esp、eip、eflags。
在Loader中,gs对应的描述符DPL为3,所以进程中的代码是有访问权限访问显存的;其他段寄存器对应的描述符基地址和段界限与先前的段寄存器对应的秒速恢复基地址金和段界限相同,只是改变它们的RPL和TI,以表示它们运行的特权级。
进程表和与之相关的TSS的对应关系如图:
mark
主要分为三个部分:

  1. 进程表和GDT。进程表内的LDTSelector对应的GDT中的一个描述符,而这个描述符所指向的内存空间就存在进程表内。
  2. 进程表和进程。进程表就是进程的描述,进程运行过程中如果被中断,各个寄存器的值都会被保存进进程表中。但是,在我们的第一个进程开始之前,并不需要初始化太多内容,只需要知道进程的入口地址就足够了。
  3. GDT和TSS。GDT中需要有一个描述符来对应TSS,需要事先初始化这个描述符。
    接着开始具体初始化,
    第一步,首先准备一个小的进程体:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void TestA()
    {
    int i = 0;
    while(1){
    disp_str("A");
    disp_int(i++);
    disp_str(".");
    delay(1);
    }
    }

这个进程体(函数)的功能就是打印一个字符并显示数字,并稍微停顿。
接着我们要注视掉hlt,并让程序跳转到kernel_main()中。
delay()函数也就是一个循环嵌套。
第二步,初始化进程表。
首先收进程表的结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;

然后在global.c中声明一个进程表:

1
PUBLIC PROCESS proc_table[NR_TASKS];

其中NR_TASKS定义了最大允许进程,我们将其设为1。为了以后扩展,我们将其还是声明成一个数组。
接着就初始化进程表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.

进程表的初始化主要有寄存器、LDTSelector和LDT。
LDTSelector被赋值为SELECTOR_LDT_FIRST。LDT中共有两个描述符,分别被初始化成内核代码段和内核数据段,只是改变一下DPL以让其运行在低的特权级下。
要初始化的寄存器比较多,其中eip指向TestA,这表名进程将从TestA的入口开始运行。
另外,esp指向了单独的栈,栈的大小为STACK_SIZE_TOTAL。
最后一行是设置eflags,0x1202恰好设置了IF位,并把IOPL设为1.这样进程就可以使用I/O指令,并且中断会在iretd执行时被打开。
第三步,准备GDT和TSS。
到此,只有TSS和它对应的描述符没有初始化了。在init_prot(),填充TSS以及对应的描述符。

启动进程

make以后,就运行成功了:
mark

第一个进程回顾

mark
从上面的进程启动的示意图可以看出,进程体TestA()在内核被LOADER放置到内存中之后就准备好了。

第二步,丰富中断处理程序

让时钟中断开始起作用

现在打开i8259.c的init_8259A(),同时设置EOI。为了让中断显示出来,我们将通过改变屏幕第0行、第0列字符的方式来说明中断例程正在运行。

现场的保护和恢复

使用进程表是为了保存进程的状态,以便中断处理程序完成之后需要被恢复的进程能够被顺利地恢复。在进程表中,我们为每一个寄存器预留了位置,以便将其保存下来,这样就可以在进程调度模块中尽情的使用这些寄存器,而不必担心会对进程产生不良影响。

赋值tss.esp0

中断的打开意味着ring0和ring1之间频繁的切换,两个层级之间的切换包含两方面,一是代码的跳转,还有一个不容忽视的地方,就是堆栈也在切换。TSS的用处知识保存ring0堆栈信息,而堆栈的信息就是ss和esp两个寄存器。由于要为先一次ring1->ring0做准备,所以用iretd返回之前要保证tss.esp0是正确的。
当进程被中断切换到内核态时,当前的各个寄存器应该被立即保存(压栈)。也就是tss.esp0应该是当前进程的进程表中保存寄存器值的地方,即struct s_proc中struct中s_stackframe的最高地址处。这样进程被挂起后才恰好保存寄存到正确的位置。
现在的中断程序变成了:在中断发生的开始,esp的值是刚刚从TSS里面取到的进程表A中regs的最高地址,然后各个寄存器值被压栈入进程表,最后esp指向regs的最低地址处,然后设置tss.esp0的值,准备下一次进程被中断时使用。

您的支持将鼓励我继续创作!