进程的切换及调度等内容是和保护模式的相关技术紧密相连的,这些代码量可能并不多,但却至关重要。

我们需要一个数据结构记录一个进程的状态,在进程要被挂起的时候,进程信息就被写入这个数据结构,等到进程重新启动的时候,这个信息重新被读出来。

在很多情况下,进程和进程调度是运行在不同的层级上的。这里本着简单的原则,我们让所有任务运行在ring1,而让进程切换运行在ring0.

诱发进程切换的原因不只一种,比较典型的情况是发生了时钟中断。但并非在每一次时钟中断时都一定会发生进程切换,不过这里为了容易理解和实现,每次中断都切换一次进程。

下面介绍一下进程切换时的情形,如下图所示:

1.进程A运行中。

2.时钟中断发生,ring1—>ring0,时钟中断处理程序启动。

3.进程调度,下一个应运行的进程(假设为进程B)被指定。

4.进程B被恢复,ring0—>ring1.

5.进程B运行中。

只有可能被改变的才有保存的必要。所以我们要把寄存器的值统统保存起来,准备进程被恢复执行时使用。

一条pushad指令可以保存许多寄存器值。

进程栈和内核栈如下图:

进程栈——进程运行时自身的堆栈。

进程表——存储进程状态信息的数据结构。

内核栈——进程调度模块运行时使用的堆栈。

对于有特权级变换的转移,如果由外层向内层转移时,需要从TSS中取得从当前TSS中取出内层ss和esp作为目标代码的ss和esp。所以我们必须事先准备好TSS。由于每个进程相对独立,我们把涉及到的描述符放在局部描述符表LDT中,所以我们还需要为每个进程准备LDT。

整个程序的大致流程是:

	sgdt	[gdt_ptr]	; cstart() 中将会用到 gdt_ptr
call cstart ; 在此函数中改变了gdt_ptr,让它指向新的GDT
lgdt [gdt_ptr] ; 使用新的GDT lidt [idt_ptr] jmp SELECTOR_KERNEL_CS:csinit

首先走kernel.asm的_start——这里调用start.c的cstart函数

cstart函数——主要是将loader中的GDT复制到新的GDT中

紧接着调用init_prot()函数——init_prot函数在protect.c中,主要是初始化8259A和全部中断门,此函数的最后是填充GDT中TSS这个描述符,紧接着填充GDT中进程的LDT的描述符。

此时cstart结束。

然后执行csinit——先加载ltr(TSS),然后进入kernel_main

kernel_main函数——是在main.c中定义的,此函数首先初始化进程的进程表的各个属性,进程表的定义在proc.h中,然后最后执行restart函数

restart函数在kernel.asm中定义,如下:

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

restart是进程调度的一部分,同时也是我们的操作系统启动第一个进程时的入口。

首先让esp指向将要运行的进程的进程表,然后加载ldt指向的是进程表的ldt_selector,restart最后两行的作用是将s_proc这个结构中第一个结构体成员regs的末地址赋给TSS中ring0堆栈指针域(esp)。我们可以想象,在下一次中断发生时,esp将变成regs的末地址,然后进程ss和esp两个寄存器值,以及eflags还有cs、eip这几个寄存器值将依次被压栈,放到regs这个结构的最后面(不要忘记堆栈是从高地址向低地址生长的),最后通过iretd指令执行并进入进程TestA。

对于p_proc_ready,编译器在编译时会产生一个符号表,记录了符号名和它的地址。对于指针变量,符号表里记录的是指针的地址,通过该地址取到所指变量的真实地址,最后取到的才是所指变量的值。

IRETD 指令先弹出一个32位的EIP值,然后再弹出一个32位值并将最低的2个字节值传入CS寄存器,最后再弹出一个32位的标志寄存器值

lea——

比如: LEA AX,BUF
就是将存储器中BUF所指的地址传送给AX.
区别MOV传送指令:
MOV传送的是地址所指的内容,而LEA只是地址。

还有从低特权级到高特权级转移的时候,需要用到TSS

p_proc_ready应该是一个指向进程表的指针,存放的便是下一个要启动进程的进程表的地址。而且其中的内容必然是以下图所示的顺序进行存放,这样才会使pop和popad指令执行后各寄存器的内容更新一遍。

p_proc_ready是一个结构类型指针:struct s_proc*。s_proc这个结构体的第一个成员也是一个结构s_stackframe,它的内容安排与我们的推断完全一致。

进程的状态统统被存放在s_proc这个结构体中,s_proc这个结构就应该是我们提到过的“进程表”。当要恢复一个进程时,便将esp指向这个结构体的开始处,然后运行一系列的pop命令将寄存器值弹出。进程表的开始位置结构图如下图所示:

接下来lldt这个指令是设置ldtr的。esp + P_LDT_SEL是s_proc中的成员ldt_sel。restart最后两行的作用是将s_proc这个结构中第一个结构体成员regs的末地址赋给TSS中ring0堆栈指针域(esp)。

一个进程开始之前,必须初始化的寄存器列表:cs、ds、es、fs、gs、ss、esp、eip、eflags。

我们在Loader中就把gs对应的描述符DPL设为3,所以进程中的代码是有权限访问显存的。

在第一个进程正式开始之前,其核心内容便是一个进程表以及与之相关的TSS等内容。如下图所示:

这个图看起来有点复杂,但是如果将其化整为零,可以分为4个部分,那就是进程表、进程体、GDT和TSS。它们之间的关系大致分为三个部分:

1.进程表和GDT。进程表内的LDT Selector对应GDT中的一个描述符,而这个描述符所指向的内存空间就存在于进程表内。

2.进程表和进程。进程表是进程的描述,进程运行过程中如果被中断,各个寄存器的值都会被保存进进程表中。但是在我们的第一个进程开始之前,并不需要初始化太多内容,只需要知道进程的入口地址就足够了。另外由于程序免不了用到堆栈,而堆栈是不受程序本身控制的,所以还需要事先指定esp。

3.GDT和TSS。GDT中需要有一个描述符来对应TSS,需要事先初始化这个描述符。

第一步,首先来准备一个小的进程体。

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

在之前我们调用指令sti打开中断之后就用hlt指令让程序停止以等待中断的发生。但在这里我得把hlt注释掉。还有由于在完成进程的编写之前,要让程序停住,所以我们用一个死循环作为它的结束。

PUBLIC int kernel_main()
{
...
while(1){}
}

第二步,初始化进程表。

要初始化进程表,首先要有进程表结构的定义,proc.h的STACK_FRAME。global.c的NR_TASKS定义了最大允许进程,我们把它设为1.初始化进程表的代码在main.c的kernel_main()函数。

进程表需要初始化的主要有3个部分:寄存器、LDT Selector和LDT。LDT Selector被赋值为SELECTOR_LDT_FIRST,LDT里面共有两个描述符,为简化起见,分别被初始化成内核代码段和内核数据段,只是改变了一下DPL以让其运行在低的特权级下。

要初始化的寄存器比较多,cs指向LDT中第一个描述符,ds、es、fs、ss都设为指向LDT中的第二个描述符,gs仍然指向显存,只是其RPL发生改变。

接下来eip指向TestA,这表明进程将从TestA的入口地址开始运行。另外esp指向了单独的栈,栈的大小为STACK_SIZE_TOTAL。

最后一行是设置eflags,0x1202恰好设置了IF位并把IOPL设为1.这样,进程就可以使用I/O指令,并且中断会在iretd执行时被打开(kernel.asm中的sti指令已经被注释掉了)。

一定要记得LDT跟GDT是联系在一起的,别忘了填充GDT中进程的LDT的描述符。如下:

	/* 填充 GDT 中进程的 LDT 的描述符 */
init_descriptor(&gdt[INDEX_LDT_FIRST],
vir2phys(seg2phys(SELECTOR_KERNEL_DS), proc_table[0].ldts),
LDT_SIZE * sizeof(DESCRIPTOR) - 1,
DA_LDT);

最后再初始化填充TSS以及对应的描述符:

	/* 填充 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); /* 没有I/O许可位图 */ ------------加载-----------------
xor eax, eax
mov ax, SELECTOR_TSS
ltr ax

由于进程的各寄存器值如今已经在进程表里面保存好了,现在我们只需要让esp指向栈顶,然后将各个值弹出就行了。最后一句iretd执行以后,eflags会被改变成pProc->regs.eflags的值。我们事先置了IF位,所以进程开始运行之时,中断其实也已经被打开了,这对以后的程序很重要。

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

启动进程(main.c),restart实现ring0->ring1的跳转:

	p_proc_ready	= proc_table;
restart();

运行如下:

源码

操作系统开发系列—13.a.进程 ●的更多相关文章

  1. 操作系统开发系列—13.c.进程之中断重入

    现在又出现了另外一个的问题,在中断处理过程中是否应该允许下一个中断发生? 让我们修改一下代码,以便让系统可以在时钟中断的处理过程中接受下一个时钟中断.这听起来不是个很好的主意,但是可以借此来做个试验. ...

  2. 操作系统开发系列—13.b.进程之丰富中断处理程序

    首先打开时钟中断: out_byte(INT_M_CTLMASK, 0xFE); // Master 8259, OCW1. out_byte(INT_S_CTLMASK, 0xFF); // Sla ...

  3. 操作系统开发系列—13.e.三进程

    我们再来添加一个任务,首先添加一个进程体: void TestC() { int i = 0x2000; while(1){ disp_str("C"); disp_int(i++ ...

  4. 操作系统开发系列—13.g.操作系统的系统调用 ●

    在我们的操作系统中,已经存在的3个进程是运行在ring1上的,它们已经不能任意地使用某些指令,不能访问某些权限更高的内存区域,但如果一项任务需要这些使用指令或者内存区域时,只能通过系统调用来实现,它是 ...

  5. 操作系统开发系列—13.i.进程调度 ●

    上面的三个进程都是延迟相同的时间,让我们修改一下,尝试让它们延迟不同的时间. void TestA() { int i = 0; while (1) { disp_str("A." ...

  6. 操作系统开发系列—13.h.延时操作

    计数器的工作原理是这样的:它有一个输入频率,在PC上是1193180HZ.在每一个时钟周期(CLK cycle),计数器值会减1,当减到0时,就会触发一个输出.由于计数器是16位的,所以最大值是655 ...

  7. 操作系统开发系列—13.d.多进程 ●

    进程此时不仅是在运行而已,它可以随时被中断,可以在中断处理程序完成之后被恢复.进程此时已经有了两种状态:运行和睡眠.我们已经具备了处理多个进程的能力,只需要让其中一个进程处在运行态,其余进程处在睡眠态 ...

  8. 微信公众号开发系列-13、基于RDIFramework.NET框架整合微信开发应用效果展示

    1.前言 通过前面一系列文章的学习,我们对微信公众号开发已经有了一个比较深入和全面的了解. 微信公众号开发为企业解决那些问题呢? 我们经常看到微信公众号定制开发.微信公众平台定制开发,都不知道这些能给 ...

  9. 操作系统开发系列—12.f.在内核中添加中断处理 ●

    因为CPU只有一个,同一时刻要么是客户进程在运行,要么是操作系统在运行,如果实现进程,需要一种控制权转换机制,这种机制便是中断. 要做的工作有两项:设置8259A和建立IDT. /*========= ...

随机推荐

  1. 170多个Ionic Framework学习资源(转载)

    在Ionic官网找到的学习资源:http://blog.ionic.io/learning-ionic-in-your-living-room/ 网上的文章比较多,但是很多时候我们很难找到自己需要的. ...

  2. 自制简单实用IoC

    IoC是个好东西,但是为了这个功能而使用类似 Castle 这种大型框架的话,感觉还是不大好 代码是之前写的,一直没详细搞,今天整理了一下,感觉挺实用的. IoC定义接口: using System; ...

  3. JS Replace() 全部替换字符的用法

    好久不写js了,今早遇到替换字符的,就浪费了点时间,由此,要记录下来.replace()方法:楼主有个字符串,需要替换掉其中的一些字母,如: var test='123helo123boy123hi' ...

  4. 扩展Exception,增加判断Exception是否为SQL引用约束异常方法!

    在设计数据表时,如果将某些列设置为关联其它表的外键,那么如果对其进行增加.修改操作时,其关联表若没有相匹配的记录则报错,或者在对其关联表进行删除时,也会报错,这就是外键约束的作用,当然除了外键还有许多 ...

  5. Html页面head标签元素的意义和应用场景

    相信在html5之前,很少人会关注html页面上head里标签元素的定义和应用场景,可能记得住的只有"title"."keyword"和"descri ...

  6. [Tool] 配置文件之Web.config

    开发人员工具: 安装完vs后,(如2013:C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\Shortcuts\VS ...

  7. VS2015 新Web项目(C#6)出现CS1617错误的解决

    VS2015新增了对C#6的支持. 在新的Web项目模板中通过引入nuget包Microsoft.CodeDom.Providers.DotNetCompilerPlatform:1.0.0并在web ...

  8. 数据库中char, varchar, nvarchar的差异

    char     char是定长的,也就是当你输入的字符小于你指定的数目时,char(8),你输入的字符小于8时,它会再后面补空值.当你输入的字符大于指定的数时,它会截取超出的字符.    nvarc ...

  9. 【UWP】批量修改图标尺寸

    UWP开发中项目用到的图标资源非常多,通常每一种图标都有几种不同的尺寸,一般来说,我的项目所有Package.appxmanifest用到的图标就有40个,通常这些图标都是一样的,只是尺寸大小不一而已 ...

  10. form表单及其中元素

    <form method=get/post action="提交路径"> 单行文本域:<input type="text" name=&quo ...