关于32位保护模式下的进程调度

前几篇学习笔记非常简短,因为想等现在手头工作差不多了来个总结性大招,但转眼一个月过去了,关于系统在保护模式下的操作实在是“仰之弥高,钻之弥坚”……心累,大招也放不出来了。

这一个月时间里,我主要是依靠这基本书在学保护模式和操作系统,有几本上一篇学习笔记中提到过了,再提一下表达我对作者大大们的谢意。

8846565-d0dd58e08619ba7f

还有CSDN上的博客,尤其是《30天》的笔记,也给了我很大帮助:http://www.cnblogs.com/bitzhuwei/p/OS-in-30-days-10-programmable-interval-timer.html

*

32位的CPU有32根地址线,所以理论上的最大内存为2^32=4GB(实际上还要连外设所以达不到这么大),不像之前20位地址线的时候只有64MB。为了方便管理内存,避免内存中的程序相互影响而被破坏而设置了保护模式,使得程序只能访问程序事先预设访问的那部分内存。

内存管理的工具是GDT(Global Descriptor Table,全局描述符表),是内存中的一块区域,位置由操作系统开发者给定,GDT的首地址和长度存放在一个寄存器GDTR中。

GDT中的元素是描述符(Descriptor),一个描述符长度为8字节,存放了某一内存段的大小、起始地址和权限。不同描述符描述的内存段可以重合。

每一个段都必须有一个对应的描述符在GDT里,描述符是那个段的名片。

权限有0、1、2、3四个等级,boot和操作系统具有从BIOS哪里继承来的最高等级:0级。用户程序一般是3级(因此用户程序想要调用操作系统的API简直麻烦,需要通过调用门什么的,心累)。

从实模式(也就是8086的模式,BIOS加载完默认的模式)跳转到保护模式需要4步:

扫描二维码关注公众号,回复: 3478256 查看本文章

1.初始化GDT和GDTR。

2.打开A20地址线。A20地址线是一个历史遗留问题,因为16位的CPU只有A0~A19地址线,这一步的存在是为了兼容在16位CPU上跑的程序。

3.关闭中断。因为16位下的中断向量表和32位下的中断向量表有差别。

4.设置PE位。

OK,接下来所有的段跳转都要遵循32位模式了,不过我们先做一个far JMP,目的是清空流水线,尽管只是跳转到下一句指令。

BOOT的大致作用:

初始化GDT——>读内核——>根据内核头部修改GDT——>跳转到操作系统的入口。

操作系统内核一般分为5个区域,头部、公用例程段、内核数据段、内核代码段、尾部,如下所示:

8846565-f80ad369246e30c4

(图摘自《从实模式到保护模式》)

其中,公用例程段给用户程序提供了系统调用的功能。Printf和中断处理程序都是在这块定义的。

当操作系统要读取并执行一个用户程序时,内代码的大致流程(参考资料为《从实模式到保护模式》):

切换到内核数据段,读取用户程序头部,了解用户程序尺寸和需要的内存数量——>

根据头部信息修改内核数据段的变量——>

调用动态分配内存的程序,动态分配内存——>

切换到0~4GB段,便于对所有内存区域进行操作——>

循环读用户程序到内存——>

分配堆栈地址——>

重写GDT和GDTR,在GDT中加入用户程序的段——>

重定位用户程序内的符号地址——>

出栈(??)——>

选择用户程序段(选择指向用户程序段的头部的选择子)——>

JMP [0X10](用户程序头部0x10处存放了用户程序的入口地址)——>

用户程序返回点——>恢复选择内核自己的核心数据段、堆栈段。

至于“重定位用户程序内的符号地址”这一步,也就是用户程序怎么把调用系统例程,当用户程序的优先级也是0的时候,大致是这么一个思路:

首先,在用户程序的头部做一个表格,就叫“U-SALT”表吧,表里面存放的是一个个系统函数名字符串,比如“@readdisk”、“@printf”……

首先的首先,内核的数据段也有一张对应的表格,就叫“C-SALT”表吧,里面每个元素是“函数名字符串+6个字节的地址表(2个字节的公用例程段的段选择子+4个字节的该程序在公用例程段的偏移地址)”。比如:“@readdisk”+地址。

然后,一一比对U-SALT和C-SALT中的字符串,如果对上了,就把C-SALT中的地址替换掉U-SALT中的字符串。

所以,U-SALT中的每个元素其实是一段特定的空间(比如256字节),那段空间的开头存放了函数名字符串。

以上是建立在用户程序可以访问公用例程段的基础上的,然而,很不幸,用户程序的优先级往往是3,访问不了,这时候,需要用到IDT(Interrupt Descriptor Table,中断描述符表),和一套复杂的升降优先级的过程,日后再说吧,如果我能搞懂的话。

8846565-1b6fb6abc7fb96c4

以上主要参考了《实模式到保护模式》,以下将主要参考《30天》,作者把自制的那个操作系统叫做“纸娃娃系统”。《30天》的写作风格和另外两本还是很不一样的,总感觉什么事情到了《30天》这里就变得很容易。

《30天》使用的编译工具是作者自己根据NASM和gcc改的,所以一定程度上简化了编程。至于究竟是怎么改了,改了之后究竟哪里不同,我也不是很清楚。这本书总体上重实践而轻理论,专业名词很少,这是它的缺点和优点,另一个特点就是它很快就引入了图形界面(包括鼠标)和C语言(第3天就引入了)。

目前只不走心地看过前16天,用户进程和系统进程暂时还是放在一个文件里的,因此暂不涉及上文中SALT表的问题。

由于引入了多进程,必须涉及定时器中断,也就是分时系统的实现,需要用到定时器中断。用定时器实现分时系统的方法大概思路如下:

首先要设置定时器,定时器是CPU的外设,是个硬件,引脚地址和设置方法查资料就能得到。比如设定定时器中断时间为10ms中断一次。

然后要设定一些Timer,这些Timer只是软件上的概念,比如每0.1秒切换一次进程。

再然后把Timer按超时时间大小排列在一张链表里,越早到达超时时刻(超时时间越近)就排在前面,越晚到达超时时刻就排在后面。在Timer列表最后再坠上一个“哨兵”,只为了程序上实现的方便。

如此Timer链表设定好了。每次定时器中断到来的时候都先检查Timer链表的第一个Timer有没有超时,如果超时,就执行相应的动作。

15天、16天的“纸娃娃系统”里面跑着4个进程,分了好几个优先级,需要注意的是:这个优先级是纸娃娃系统自己定义的,和前文说到的0、1、2、3优先级没关系,GDT中默认优先级都是0。

此处“纸娃娃系统”定义的优先级又有两个等级,一个是lever,一个是priority,lever档次更高:高lever中的进程运行时,屏蔽所有低lever进程;而同一lever中有好几个priority,priority越高,分到的时间片长度越长。

主进程,也是优先级最高的进程,跑的是键盘鼠标的相应。采用一个FIFO的队列作为键盘和鼠标中断缓冲区,主进程的lever最高,所以当FIFO队列中有数据时,键鼠中断唤醒进程,进程可以得到立即执行,而FIFO中没有数据时,线程自动休眠,将CPU让给低lever的进程。

剩下3个进程跑的程序都是一样的,不同的只是priority,因此有的进程时间片更长,宏观上执行的更快。

由于需要使用多任务,用到了一种结构体TSS(Task State Segment,任务状态段),Intel公司建议,每个任务都有一个TSS,TSS是任务存在的标志,它存储任务的当前时刻几个寄存器的值,尤其是程序寄存器CS和SS的值。

TSS是内存中存在的一个段,所以在GDT中注册有它的位置。

当前正在运行的任务的TSS的地址存放在任务寄存器TR中。

进程切换时,段选择子指向TSS,偏移地址可以是任意值,一般写0000即可,CPU能判断段选择子究竟指向的是一段其他程序还是TSS。在任务切换过程中,首先,处理器中各寄存器的当前值被自动保存到TR所指定的TSS中;然后,下一任务的TSS的选择子被装入TR;最后,从TR所指定的TSS中取出各寄存器的值送到处理器的各寄存器中(摘自紫陌的博客:https://www.cnblogs.com/guanlaiy/archive/2012/10/25/2738355.html)。

然而Linux的对TSS的用法略有不同,不是每个任务一个TSS,TR随着任务切换而改变,而是TR初始化之后就不变了,所有任务共享一个TSS,而任务的信息存放在另一个结构体thread_struct中(参考资料:http://blog.csdn.net/nodeathphoenix/article/details/39269997)

因为理解不深,这篇文章可能有诸多问题,回头再慢慢抿回来就是了。

顺便我越来越觉得写这本书的川合秀实大大是个日本萌妹,但是网上没有他的资料。

猜你喜欢

转载自blog.csdn.net/m0_37946085/article/details/82920655
今日推荐