转载:http://tieba.baidu.com/p/1273477757

0 neta

有的时候我们在读书或者看文档。
——啊,原来这东西的框架就是这样而已,很直白么。
有的时候我们在读代码。
——于是也不免有一点抱怨:作者多写一点注释又不会累死。
(尤其是在作者花了相当篇幅威胁某个禁止他测试DDoS attacker的管理员但懒得多写点解释的情况下。)
不过读完了代码稍微回顾一下的话,又会发现〔其实这里那里的根本就不需要注释〕。
如果只求行尸走肉般的实现,OS也是这么一种东西。

有的时候我会想起以前从某个机房窗口看见延伸到远处的两排路灯,不过可惜现在看不见了。
如果有什么是要顺便提一句的话,那大概是vj也关掉了。
另外有什么让人没法悠闲享受的,说不定就是写代码时候的忐忑吧。
当我写下一个struct iovec的时候确实想着〔嗯,就这样〕,不过一个PCB却总像个Claymore.
也确实一贯被炸得四散飞出去。
那么比我更有觉悟一些的诸君可以看下去了,因为我并不会给出完整的代码,这毕竟也只是玩乐的note.
有人说,tutorial让你〔有信心做下去〕,所以我也不为汗牛充栋的教程再抹一笔黑,想到哪里写哪里而已。
如果希望自制OS, 请在参考一篇tutorial的基础上阅读。厕上亦可。

事先提一下,这里的体系针对i386, 并没有考虑64位机和多CPU的情况,协处理器也被我无视了,请小心鳄鱼。
有关引导、装载和GDT的事情,亚里亚酱的某篇文章里提及过,不再赘述。

1 地址空间(一)

手册性质的内容不打算多写了,机制上看来无非是进行一次线性地址到物理地址的映射。
从内核对象的角度看来,一个页目录实体对应一个独立的地址空间,这是构成〔进程〕的基础之一。
如果和只使用一个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, 后面会有涉及。)
之后的步骤,用很伪的汇编描述像是:

schedule:
    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族。
先给出一段伪代码:

procedure fork;
    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的更多相关文章

  1. 一步步写STM32 OS【四】OS基本框架

    一.上篇回顾 上一篇文章中,我们完成了两个任务使用PendSV实现了互相切换的功能,下面我们接着其思路往下做.这次我们完成OS基本框架,即实现一个非抢占式(已经调度的进程执行完成,然后根据优先级调度等 ...

  2. 一步步写STM32 OS【一】 序言

    一直想写个类似uCOS的OS,近段时间考研复习之余忙里偷闲,总算有点成果了.言归正传,我觉得OS最难的部分首先便是上下文切换的问题,他和MCU的架构有关,所以对于不同的MCU,这部分需要移植.一旦这个 ...

  3. 一步步写STM32 OS【三】PendSV与堆栈操作

    一.什么是PendSV PendSV是可悬起异常,如果我们把它配置最低优先级,那么如果同时有多个异常被触发,它会在其他异常执行完毕后再执行,而且任何异常都可以中断它.更详细的内容在<Cortex ...

  4. 一步步写STM32 OS【二】环境搭建

    一.安装IAR for ARM6.5 二.新建工程 1.选择处理器:STM32F407VG,暂不使用FPU 2.必要的路径配置和宏定义 3.使用SWO重定向IO输出 4.使用ST-LINK仿真器 5. ...

  5. x01.os.12: 在 windows 中写 OS

    在 windows 中写操作系统,需要一系列的辅助工具.在此,要感谢川谷秀实!所有工具,都在 z_tools 文件夹中.有了大师的帮助,不妨也来尝试在 windows 中写一把 OS. 源代码及工具可 ...

  6. iOS冰与火之歌(番外篇) - 基于PEGASUS(Trident三叉戟)的OS X 10.11.6本地提权

    iOS冰与火之歌(番外篇) 基于PEGASUS(Trident三叉戟)的OS X 10.11.6本地提权 蒸米@阿里移动安全 0x00 序 这段时间最火的漏洞当属阿联酋的人权活动人士被apt攻击所使用 ...

  7. golang os.OpenFile

    os.O_WRONLY | os.O_CREATE | O_EXCL           [如果已经存在,则失败] os.O_WRONLY | os.O_CREATE                 ...

  8. 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 ...

  9. (原创)Python文件与文件系统系列(2)——os模块对文件、文件系统操作的支持

    os模块的功能主要包括文件系统部分和进程管理部分,这里介绍其中与文件系统相关的部分. 当请求操作系统执行操作失败时,os模块抛出内置异常 exceptions.OSError 的实例,可以通过 os. ...

随机推荐

  1. 【转】关于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 ...

  2. VS2010之MFC串口通信的编写教程--转

    http://wenku.baidu.com/link?url=K1XPdj9Dcf2of_BsbIdbPeeZ452uJqiF-s773uQyMzV2cSaPRIq6RddQQH1zr1opqVBM ...

  3. Struct2(三) Struct2 标签

    在上一篇 Struct2(二)中,我们新建了工程Struct2test用来验证hello World 程序,在index.jsp中,我们添加了一个Struct2 uri 标签用来创建一个指向hello ...

  4. 详解HashMap的内部工作原理

    本文将用一个简单的例子来解释下HashMap内部的工作原理.首先我们从一个例子开始,而不仅仅是从理论上,这样,有助于更好地理解,然后,我们来看下get和put到底是怎样工作的. 我们来看个非常简单的例 ...

  5. ViewPager顶部标题控件PagerSlidingTabStrip

    最近搞一个项目,要求做一个和网易新闻顶部菜单的滑动效果,如图: 顶部标题中下面有个红色的矩形小条,左右滑动时会跟随手势动态滑动,效果很绚丽,唉,特效啊! 自己搞了一上午无果,还是是github上找大神 ...

  6. oracle 同样数据删除(仅仅留一条)

    DELETE FROM reg_user t1 WHERE user_name='9527008' and rowid > ( SELECT min(rowid) FROM location t ...

  7. c++11 : range-based for loop

    0. 形式 for ( declaration : expression ) statement 0.1 根据标准将会扩展成这样的形式: 1   { 2     auto&& __ra ...

  8. CentOS6.6(单用户模式)重设root密码

    1.开机时手要快按任意键,因为默认时间5s 2.grub菜单,只有一个内核,没什么好上下选的,按e键.不过如果你升级了系统或安装了Xen虚拟化后,就会有多个显示了. 3.接下来显示如下,选择第二项,按 ...

  9. 【转】iOS开发24:使用SQLite3存储和读取数据

    转自:http://my.oschina.net/plumsoft/blog/57626 SQLite3是嵌入在iOS中的关系型数据库,对于存储大规模的数据很有效.SQLite3使得不必将每个对象都加 ...

  10. python 安装 memcache

    方式一: python3 -m pip install python-memcached 方式二: pip3 install python-memcached 方式三: tar zxf python- ...