〔写在OS边上〕定性note
转载:http://tieba.baidu.com/p/1273477757
有的时候我们在读书或者看文档。
——啊,原来这东西的框架就是这样而已,很直白么。
有的时候我们在读代码。
——于是也不免有一点抱怨:作者多写一点注释又不会累死。
(尤其是在作者花了相当篇幅威胁某个禁止他测试DDoS attacker的管理员但懒得多写点解释的情况下。)
不过读完了代码稍微回顾一下的话,又会发现〔其实这里那里的根本就不需要注释〕。
如果只求行尸走肉般的实现,OS也是这么一种东西。
有的时候我会想起以前从某个机房窗口看见延伸到远处的两排路灯,不过可惜现在看不见了。
如果有什么是要顺便提一句的话,那大概是vj也关掉了。
另外有什么让人没法悠闲享受的,说不定就是写代码时候的忐忑吧。
当我写下一个struct iovec的时候确实想着〔嗯,就这样〕,不过一个PCB却总像个Claymore.
也确实一贯被炸得四散飞出去。
那么比我更有觉悟一些的诸君可以看下去了,因为我并不会给出完整的代码,这毕竟也只是玩乐的note.
有人说,tutorial让你〔有信心做下去〕,所以我也不为汗牛充栋的教程再抹一笔黑,想到哪里写哪里而已。
如果希望自制OS, 请在参考一篇tutorial的基础上阅读。厕上亦可。
事先提一下,这里的体系针对i386, 并没有考虑64位机和多CPU的情况,协处理器也被我无视了,请小心鳄鱼。
有关引导、装载和GDT的事情,亚里亚酱的某篇文章里提及过,不再赘述。
手册性质的内容不打算多写了,机制上看来无非是进行一次线性地址到物理地址的映射。
从内核对象的角度看来,一个页目录实体对应一个独立的地址空间,这是构成〔进程〕的基础之一。
如果和只使用一个GDT的角度看来,这样的寻址方式使我们从表示上避开了恶心的段内位移。
——虽然EA仍然是EA, 不过已经可以干净地连续使用了。所以以后我也不会特别提及EA这术语。
(注意到内核/用户代码段和内核/用户数据段的基和限是一样的,配合粒度就可以统一覆盖4GB)
但是另一件事情更麻烦一些。请不要忘记我们还是要执行内核代码、读取内核映像中的数据。
不妨假设内核被加载在了1MB处,延伸向高地址。总之很容易从自定的链接脚本中获得映像的始末地址。
我们需要在完成分页后仍然可以正确找到这些代码或者数据。
一个偷懒的方法,是对内核映像进行一次等值映射,也即位于物理地址p的页被映射到线性地址p.
等值映射的具体方案取决于页面分配的方式和时机,不过无论如何,强行分配页表也一样可行。
对于一个first-fit的页面分配器来说,容易想到在创建页目录对象之后第一时间分配映像页面。
另一个方法在布局上更干净一些,即将物理地址p的页分配到线性地址p+d处。
这样的做法,是为了保证OS的虚拟地址空间布局被完整地划分为用户区与内核区,例如3G/1G布局。
但是优雅的代价,是至少需要修改指令指针。我没有做这样的实现,想来还是在没有栈桢的情况下进行比较好。
除去内核,还有一部分数据是很可能需要等值映射的,即1MB以下的部分。
一个明显的例子是VGA内存映射区。
进行等值映射的时候需要小心:在获取页表项的时候可能创建新页表,这导致需要等值映射的区域被扩张。
(注意到这时我们还没有heap, 毕竟在没有启用分页的时候创建heap并不合适。)
下面是后话:
一个pitfall, 要提醒熟悉fork语义的诸君小心。
请一定记得全局变量是内核映像的一部分,是被等值映射的。
——而内核映像的页面很可能被链接到每个虚拟地址空间,而不是复制过去。
另一个pitfall, 请为用户栈(同时也是实现了cpl3切换之前的中断栈和内核栈)保留一些固定的页面。
并且确保它们在地址空间复制的时候确实被复制了。
(下面的内容,我将不会区分内核栈和中断栈,中断和系统调用共用一个运行时栈)
2 进程(一)
关于进程的定义五花八门,不过总之也脱离不了程序、数据和上下文,想必诸君也有一个版本烂熟于心。
所以要特别指出的只有一点:进程具有独立的虚拟地址空间。
进程的虚拟空间中,只有OS和自身。
如果还记得之前提及的虚拟地址空间布局,大概就能联系起来了。
——OS映像和内核堆被所有进程共享,位于线性地址中的内核区。其他都是自由使用的用户区。
然而也要记得这只是寻址时的福利,致命的页面错误仍然可能把出错的进程拉回某个现实。
另一朵渐欲迷人眼的奇葩是所谓的PCB. 当然我不知为何不太喜欢*CB这种叫法。
PCB中应当包含一系列进程的特征信息和资源信息,以及进程的上下文信息。
上下文信息用于进程的切换,其实也不用太多。基本的切换,保存esp/ebp/cr3就很充分了。
于是按照传统的创世纪步骤,我们需要手工创建第一个进程。
工序很简单:创建一个PCB, 将其初始化,同时使得时钟中断知道自己应当进行上下文切换了。
一个简单的例子,不妨假设系统中只存在就绪队列。
下面是一个比较需要磨合的部分:上下文切换。
首先考虑下我们最需要切换的寄存器:
——esp应当指向上升进程的内核栈顶;
——ebp应当指向上升进程的上一个栈桢;
——cr3应当保存上升进程的页目录,以便正确完成地址映射;
——eip应当指向某个断点,上升进程得以从此继续执行。
然后考虑下切换的顺序:
——因为需要保存下降进程的寄存器上下文,所以cr3的切换时机取决于内核的布局;
——在切换eip之前需要切换到上升进程的esp和ebp;
——esp和ebp的切换顺序取决于PCB中保存的寄存器上下文。
这里提出一个示例方案:
PCB中保存了esp/ebp/cr3/eip四种上下文,但ebp在切换上下文时保存在下降进程的内核栈上。
(至于为何还要在PCB中保存ebp, 后面会有涉及。)
之后的步骤,用很伪的汇编描述像是:
push ebp
mov [下降进程PCB的esp字段], esp
mov dword [下降进程PCB的eip字段], .bpoint
mov cr3, ecx
mov esp, [上升进程PCB的esp字段]
push dword [上升进程PCB的eip字段]
jmp __switch_to
.bpoint:
pop ebp
ret
__switch_to:
ret
直到第七行之前的目的显而易见。此后的push-jmp-ret代码构造了一对call-ret, 使得eip置为上升进程的断点。
如果仅仅是单纯的进程切换,上升进程的断点必然是.bpoint处。另一种情况在fork时发生,后述。
这样的上下文切换使得下降进程进入schedule之后,上升进程从schedule返回。
这个模仿,来自粗口林的实现。他的__switch_to完成了一部分协处理器上下文的处理。
如果觉得有什么不安的地方,也可以在内核栈上保存esi和edi.
下面是另一个或许有点令人困扰的部分。
啊没错,如果诸君还记得某2238行的/* you are not expected to understand this */就更好了。
于是直到现在我还是觉得aret和aretu这样的例程名很帅气的。
关子卖到此为止。下面的实现是fork. 我们需要复制父进程的地址空间,以产生一个新进程。
关于fork的性能也有一些讨论,但是这里都略去不表。既没有写时复制,也没有vfork来配合exec族。
先给出一段伪代码:
cli;
allocate a PCB new_pcb from kernel heap;
set attributes of new_pcb (pid, page directory, status);
bpoint := read_eip();
if (the running process is the parent) then
esp := esp of parent process (not esp from pcb of the parent);
ebp := ebp of parent process (not ebp from pcb of the parent);
new_pcb->esp := esp;
new_pcb->ebp := ebp;
new_pcb->eip := bpoint;
sti;
return new_pcb->pid;
else
mov ebp, new_pcb->ebp;
sti if necessary;
return 0;
有几个部分需要澄清。
首先是read_eip(). 这个例程需要取得的断点bpoint应当是read_eip的返回地址。
如果对之前的call-ret还有印象的话,结合cdecl的约定不难考虑到read_eip应当将返回地址传送给eax.
实现的方式同样不止一种,但pop-jmp的组合是最直白的。
下面考虑实际的执行流程。父进程调用fork的时候,执行read_eip单纯只是赋值而已。
父进程将会补完子进程的PCB, 之后很可能(如果不被切换)单纯地返回。
然而注意到被补完的PCB中,断点信息变成了read_eip的返回地址。
不妨假设目前只有两个进程轮换占有CPU. 父进程的时间片耗尽,在时钟中断上被切换。
这时,__switch_to直接返回到了bpoint := read_eip();之后,仿佛从read_eip返回。
换言之这算是个废止性的返回,上升的子进程并没有pop出ebp, 而是从PCB中取出ebp补完切换。
如果诸君对废止性返回的合理稍微有一点疑问,不妨再考虑下地址空间的状况。
进入schedule的是从fork返回的父进程,而子进程的地址空间只有父进程到fork为止的栈桢。
故而这里子进程处理了schedule剩下的代码反而是个错误,对它来说schedule开始的若干桢是不存在的。
(这些栈桢很可能包括了中断上下文、时钟中断处理例程和调度例程。)
所以有一个pitfall: schedule的处理不应当使用运行时栈存取PCB数据。
或者更直白地说,我们最好采用某种使用少量通用寄存器传参的调用约定声明并实现schedule.
这样我粗糙地论证了一下此处的废止性返回是可用的。于是,子进程按照流程应当返回0。
于是我们在两个地址空间中,观察到了同一调用的两个返回值pid和0。
到这里,维持生命体征所必要的进程部分基本完备了。
没有涉及到的部分集中在进程队列上,不过比起前面的两种脏活算是小菜一碟。
不过这里一定要注意,使用的全局变量最好限于唯一的内核对象。原因请参考前一节某处。
〔写在OS边上〕定性note的更多相关文章
- 一步步写STM32 OS【四】OS基本框架
一.上篇回顾 上一篇文章中,我们完成了两个任务使用PendSV实现了互相切换的功能,下面我们接着其思路往下做.这次我们完成OS基本框架,即实现一个非抢占式(已经调度的进程执行完成,然后根据优先级调度等 ...
- 一步步写STM32 OS【一】 序言
一直想写个类似uCOS的OS,近段时间考研复习之余忙里偷闲,总算有点成果了.言归正传,我觉得OS最难的部分首先便是上下文切换的问题,他和MCU的架构有关,所以对于不同的MCU,这部分需要移植.一旦这个 ...
- 一步步写STM32 OS【三】PendSV与堆栈操作
一.什么是PendSV PendSV是可悬起异常,如果我们把它配置最低优先级,那么如果同时有多个异常被触发,它会在其他异常执行完毕后再执行,而且任何异常都可以中断它.更详细的内容在<Cortex ...
- 一步步写STM32 OS【二】环境搭建
一.安装IAR for ARM6.5 二.新建工程 1.选择处理器:STM32F407VG,暂不使用FPU 2.必要的路径配置和宏定义 3.使用SWO重定向IO输出 4.使用ST-LINK仿真器 5. ...
- x01.os.12: 在 windows 中写 OS
在 windows 中写操作系统,需要一系列的辅助工具.在此,要感谢川谷秀实!所有工具,都在 z_tools 文件夹中.有了大师的帮助,不妨也来尝试在 windows 中写一把 OS. 源代码及工具可 ...
- iOS冰与火之歌(番外篇) - 基于PEGASUS(Trident三叉戟)的OS X 10.11.6本地提权
iOS冰与火之歌(番外篇) 基于PEGASUS(Trident三叉戟)的OS X 10.11.6本地提权 蒸米@阿里移动安全 0x00 序 这段时间最火的漏洞当属阿联酋的人权活动人士被apt攻击所使用 ...
- golang os.OpenFile
os.O_WRONLY | os.O_CREATE | O_EXCL [如果已经存在,则失败] os.O_WRONLY | os.O_CREATE ...
- High Precision Timers in iOS / OS X
High Precision Timers in iOS / OS X The note will cover the do's and dont's of using high precision ...
- (原创)Python文件与文件系统系列(2)——os模块对文件、文件系统操作的支持
os模块的功能主要包括文件系统部分和进程管理部分,这里介绍其中与文件系统相关的部分. 当请求操作系统执行操作失败时,os模块抛出内置异常 exceptions.OSError 的实例,可以通过 os. ...
随机推荐
- 【转】关于Adapter的The content of the adapter has changed问题分析 关于Adapter的The content of the adapter has changed问题分析
原文网址:http://www.cnblogs.com/monodin/p/3874147.html 1.问题描述 1 07-28 17:22:02.162: E/AndroidRuntime(167 ...
- VS2010之MFC串口通信的编写教程--转
http://wenku.baidu.com/link?url=K1XPdj9Dcf2of_BsbIdbPeeZ452uJqiF-s773uQyMzV2cSaPRIq6RddQQH1zr1opqVBM ...
- Struct2(三) Struct2 标签
在上一篇 Struct2(二)中,我们新建了工程Struct2test用来验证hello World 程序,在index.jsp中,我们添加了一个Struct2 uri 标签用来创建一个指向hello ...
- 详解HashMap的内部工作原理
本文将用一个简单的例子来解释下HashMap内部的工作原理.首先我们从一个例子开始,而不仅仅是从理论上,这样,有助于更好地理解,然后,我们来看下get和put到底是怎样工作的. 我们来看个非常简单的例 ...
- ViewPager顶部标题控件PagerSlidingTabStrip
最近搞一个项目,要求做一个和网易新闻顶部菜单的滑动效果,如图: 顶部标题中下面有个红色的矩形小条,左右滑动时会跟随手势动态滑动,效果很绚丽,唉,特效啊! 自己搞了一上午无果,还是是github上找大神 ...
- oracle 同样数据删除(仅仅留一条)
DELETE FROM reg_user t1 WHERE user_name='9527008' and rowid > ( SELECT min(rowid) FROM location t ...
- c++11 : range-based for loop
0. 形式 for ( declaration : expression ) statement 0.1 根据标准将会扩展成这样的形式: 1 { 2 auto&& __ra ...
- CentOS6.6(单用户模式)重设root密码
1.开机时手要快按任意键,因为默认时间5s 2.grub菜单,只有一个内核,没什么好上下选的,按e键.不过如果你升级了系统或安装了Xen虚拟化后,就会有多个显示了. 3.接下来显示如下,选择第二项,按 ...
- 【转】iOS开发24:使用SQLite3存储和读取数据
转自:http://my.oschina.net/plumsoft/blog/57626 SQLite3是嵌入在iOS中的关系型数据库,对于存储大规模的数据很有效.SQLite3使得不必将每个对象都加 ...
- python 安装 memcache
方式一: python3 -m pip install python-memcached 方式二: pip3 install python-memcached 方式三: tar zxf python- ...