前几篇学习笔记非常简短,因为想等现在手头工作差不多了来个总结性大招,但转眼一个月过去了,关于系统在保护模式下的操作实在是“仰之弥高,钻之弥坚”……心累,大招也放不出来了。
这一个月时间里,我主要是依靠这基本书在学保护模式和操作系统,有几本上一篇学习笔记中提到过了,再提一下表达我对作者大大们的谢意。
还有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步:
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个区域,头部、公用例程段、内核数据段、内核代码段、尾部,如下所示:
(图摘自《从实模式到保护模式》)
其中,公用例程段给用户程序提供了系统调用的功能。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,中断描述符表),和一套复杂的升降优先级的过程,日后再说吧,如果我能搞懂的话。
以上主要参考了《实模式到保护模式》,以下将主要参考《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)
因为理解不深,这篇文章可能有诸多问题,回头再慢慢抿回来就是了。
顺便我越来越觉得写这本书的川合秀实大大是个日本萌妹,但是网上没有他的资料。