电子文档交易市场
安卓APP | ios版本
电子文档交易市场
安卓APP | ios版本

30天自制操作系统

74页
  • 卖家[上传人]:桔****
  • 文档编号:471
  • 上传时间:2016-11-03
  • 文档格式:PDF
  • 文档大小:4.71MB
  • / 74 举报 版权申诉 马上下载
  • 文本预览
  • 下载提示
  • 常见问题
    • 1、 着手开发之前 前言 何谓操作系统 开发操作系统的各种方法 无知则无畏 如何开发操作系统 操作系统开发中的困难 学习本书时的注意事项(重要! ) 各章内容摘要 1 前言 现在,挑选自己喜欢的配件来组装一台世界上独一无二的、个性化的PC(个人电脑)对我们来说已不再困难。不仅如此,只要使用合适的编译器,我们就可以自己编写游戏、制作自己的工具软件;使用网页制作工具,我们还可以轻而易举地制作主页;如果看过名著CPU制作法的话,就连自制CPU也不在话下。 然而,在“自制领域”里至今还有一个无人涉足的课题自己制作操作系统(OS),它看起来太难以至于初学者不敢轻易挑战。电脑组装也好,游戏、工具软件制作也好,主页也好,CPU也好,这些都已经成为初学者能够尝试的项目,而唯独操作系统被冷落在一边,实在有些遗憾。 “既然还没有这样的书,那我就来写一本。 ”这就是笔者撰写本书的初衷。 也许是因为面向初学者的书太少的缘故吧,一说起操作系统,大家就会觉着那东西复杂得不得了,简直是高深莫测。特别是像Windows和Linux这些操作系统,庞大得一张光盘都快装不下了,要是一个人凭着兴趣来开发的话,不知道需要历经多么漫

      2、长的过程才能完成。笔者也认为,像这么复杂的操作系统,单凭一个人来做,一辈子都做不出来。 英文为compiler,指能够将源代码编译成机器码的软件。 CPU制作法 ,渡波郁著,每日Communications出版公司,ISBN 4-8399-0986-5。 Operating System的缩写,汉语译作“操作系统” 。Windows、Linux、MacOS、MS-DOS等软件的总称。 第 0 天 1 2 第 0天:着手开发之前 不过大家也不必担心太多。笔者就成功地开发过一个小型操作系统,其大小还不到80KB。麻雀虽小,五脏俱全,这个操作系统的功能还是很完整的。有人也许会怀疑: “这么小的操作系统,是不是只有命令行窗口啊?要不就是没有多任务?”不,这些功能都有。 怎么样,只有80KB的操作系统,大家不觉得稍作努力就可以开发出来吗?即使是初学者,恐怕也会觉得这不是件难事吧?没错,我们用一个月的时间就能写出自己的操作系统!所以大家不用想得太难,我们轻轻松松地一起来写写看吧。 以本书作者为主角开发的操作系统OSASK 大家一听到编译后的文件大小为80KB可能会觉得它作为程序来讲已经很小了,不过

      3、曾经编过程序的人可以查一查自己编的程序(.exe文件)的大小,这样就能体会到80KB到底是难是易了。 kilobyte,程序及数据大小的度量单位,1字节 (byte)的1024倍。 一张软盘的容量是1440KB。顺便提一下,1024KB等于1MB(兆字节) 。1字节是8个比特,正好能记录8位0和1的信息。B到底是指字节(byte) ,还是指比特(bit) ,有时容易混淆。这里根据一般的规则,用大写B表示字节,小写b表示比特。 console,通过键盘输入命令的一种方式,基本上只用文字进行计算机操作,是MS-DOS等老式操作系统的主流操作方式。 在操作系统的世界里, 运行中的程序叫做 “任务” , 而同时执行多个任务的方式就被称为 “多任务”(multitask) 。 笔者与他人一起合作开发的操作系统(趁机宣传一下) 。虽然只有小小的78KB,不过为了做它也花了好几年的时间。而这次能在短时间内开发完成操作系统,是因为我们较好地总结了开发操作系统所必要的知识。也就是说,如果笔者在年轻时可以看到现在这本书的话,可能在短时间内就能开发出OSASK了,所以笔者很羡慕大家呀。 2 何谓操作系统 3

      4、 0 3 20 21 8 10 16 9 22 7 12 11 13 15 14 15 没编过程序的人也可以下载一个看上去不是很复杂的自由软件, 看看它的可执行文件有多大。Windows 2000的计算器程序大约是90KB,大家也可以根据这个想象一下。 本书对于不打算自己写操作系统,甚至连想都没想过这个问题的人来说也会大有裨益。举个例子,读本自己组装PC的书就能知道PC是由哪些组件构成的,PC的性能是由哪些部分决定的;读本如何编写游戏的书,就能明白游戏是怎样运行的;同理,读了本书,了解了操作系统的开发过程, 就能掌握操作系统的原理。 所以说, 对操作系统有兴趣的人, 哪怕并不想自己做一个出来,也可以看看这本书。 阅读本书几乎不需要相关储备知识,这一点稍后还会详述。不管是用什么编程语言,只要是曾经写过简单的程序,对编程有一些感觉,就已经足够了(即使没有任何编程经验,应该也能看懂) ,因为这本书主要就是面向初学者的。书中虽然有很多C语言程序,但实际上并没有用到很高深的C语言知识,所以就算是曾经因为C语言太难而中途放弃的人也不用担心看不懂。当然,如果具备相关知识的话,理解起来会相对容易一些,

      5、不过即使没有相关知识也没关系,书中的说明都很仔细,大家可以放心。 本书以IBM PC/AT兼容机(也就是所谓的Windows个人电脑)为对象进行说明。至于其他机型,比如Macintosh(苹果机)或者PC-9821等,虽然本书也参考了其中某些部分,但基本上无法开发出在这些机型上运行的操作系统,这一点还请见谅。严格地说,不是所有能称为AT兼容机的机型都可以开发我们这个操作系统,我们对机器的配置要求是CPU高于386(因为我们要开发32位操作系统) 。换句话说,只要是能运行Windows 95以上操作系统的机器就没有问题,况且现在市面上(包括二手市场)恐怕都很难找到Windows 95以下的机器了,所以我们现在用的机型一般都没问题。 另外,大家也不用担心内存容量和硬盘剩余空间,我们需要使用的空间并不大。只要满足以上条件,就算机器又老又慢,也能用来开发我们的操作系统。 2 何谓操作系统 说老实话,其实笔者也不是很清楚。估计有人会说: “连这个都不懂,还写什么书?”不好意思笔者见过很多种操作系统,有的功能非常多,而有的功能特别少。在比较了各种操作系统之后,笔者还是没有找到它们功能的共同点,无法

      6、下定义。结果就是,软件作者坚持说自己做的就是操作系统,而周围的人也不深究,就那样默认了,以至于什么软件都可以算是操作系统。笔者现在就是这么认为的。 既然就操作系统而言各有各的说法,那笔者也可以反过来利用这一点,一开始就根据自己的需要来定义操作系统,然后开发出一个满足自己定义条件的软件就可以了。这当然也算是开发操 本书所讲的操作系统内容仅用Macintosh是开发不了的,并且开发出的操作系统也不能直接在Macintosh上运行。但是在PC上开发的操作系统,可以通过模拟器在Macintosh上运行。 2 4 第 0天:着手开发之前 作系统了。哪怕做一个MS-DOS那样的,在一片漆黑的画面上显示出白字,输入个命令就能执行的操作系统也可以,这对笔者来说很简单。 但这样肯定会让一些读者大失所望。现在初学者也都见多识广,一提到操作系统,大家就会联想到Windows、Linux之类的庞然大物,所以肯定期待自制操作系统至少能任意显示窗口、实现鼠标光标控制、同时运行几个应用程序,等等。所以为了满足读者的期待,我们这次就来开发一个具有上述功能的操作系统。 3 开发操作系统的各种方法 开发操作系统的方法也是

      7、各种各样的。 笔者认为,最好的方法就是从既存操作系统中找一个跟自己想做的操作系统最接近的,然后在此基础上加以改造。这个方法是最节省时间的。 但本书却故意舍近求远,一切从零开始,完完全全是自己从头做起,这是因为笔者想向各位读者介绍从头到尾开发操作系统的全过程。如果我们找一个现成的操作系统,然后在此基础上删删改改的话,那这本书就不能涉及操作系统全盘的知识了,这样肯定无法让读者朋友满意。不过由于是全部从零做起,所以篇幅长些,还请读者朋友们耐下心来慢慢看。 要开发操作系统,首先遇到的问题就是使用什么编程语言,这次我们想以C语言为主。 “啊,C语言啊?” 笔者仿佛已经听到大家抱怨的声音了 (苦笑) 。 “这都什么年代了, 用C语言多土啊” 、“用C+多好呀” 、 “还是Java好” 、 “不,我就喜欢Delphi” 、 “我还是觉得Visual Basic最好”大家个人喜好习惯各不相同。这种心情笔者都能理解,但为了讲解时能简单一些,笔者还是想用C语言,请大家见谅。C语言功能虽不多,但用起来方便,所以用来开发操作系统刚好合适。要是用其他语言的话,仅讲解语言本身就要花很长时间,大家恐怕就没兴趣看下去

      8、了。 在这里先向大家传授一个从零开始开发操作系统的诀窍, 那就是不要一开始就一心想着要开发操作系统,先做一个有点操作系统样子的东西就行了。如果我们一上来就要开发一个完整的操作系统的话,要做的东西太多,想想脑袋都大了,到时恐怕连着手的勇气也没有了。笔者就是因为这个,几年间遇到了很多挫折。所以在这本书里,我们不去大张旗鼓地想着要开发一个操作系统,而是编写几个像操作系统的演示程序就行了。其实在开发演示程序的过程中大家就会逐步发现,演示程序不再是简单的演示程序,而是越来越像一个操作系统了。 4 无知则无畏 当我们打算开发操作系统时,总会有人从旁边跳出来,罗列出一大堆专业术语,问这问那,像内核怎么做啦,外壳怎么做啦,是不是单片啦,是不是微内核啦,等等。虽然有时候提这些问 演示程序的英文是demonstration。指不是为了使用,而是为了演示给人看的软件。 3 4 4 无知则无畏 5 0 3 20 21 8 10 16 9 22 7 12 11 13 15 14 15 题也是有益的,但一上来就问这些,当然会让人无从回答。 要想给他们一个满意答复,让他们不再从旁指手画脚的话,还真得多学习,拿出点像

      9、模像样的见解才行。但我们是初学者,没有必要去学那些麻烦的东西,费时费力且不说,当我们知道现有操作系统在各方面都考虑得如此周密的时候,就会发现自己的想法太过简单而备受打击没了干劲。如果被前人的成果吓倒,只用这些现有的技术来做些拼拼凑凑的工作,岂不是太没意思了。 所以我们这次不去学习那些复杂的东西,直接着手开发。就算知道一大堆专业术语、专业理论,又有什么意思呢?还不如动手去做,就算做出来的东西再简单,起码也是自己的成果。而且自己先实际操作一次, 通过实践找到其中的问题, 再来看看是不是已经有了这些问题的解决方案,这样下来更能深刻地理解那些复杂理论。不管怎么说,反正目前我们也无法回答那些五花八门的问题,倒不如直接告诉在一旁指手画脚的人们:我们就是想用自己的方法做自己喜欢的事情,如果要讨论高深的问题,就另请高明吧。 其实反过来看,什么都不知道有时倒是好事。正是因为什么都不知道,我们才可能会认真地去做那些专家们嗤之以鼻的没意义的“傻事” 。也许我们大多时候做的都没什么意义,但有时也可能会发掘出专家们千虑一失的问题呢。专家们在很多方面往往会先入为主,甚至根本不去尝试就断定这也不行那也不行,要么就浅

      10、尝辄止。因此能够挑战这些问题的,就只有我们这种什么都不知道的门外汉。任何人都能通过学习成为专家,但是一旦成为专家,就再也找不回门外汉的挑战精神了。 所以从零开始, 在没有各种条条框框限制的情况下, 能做到什么程度就做到什么程度,碰壁以后再回头来学习相关知识,也为时未晚。 实际上笔者也正是这样一路磕磕绊绊地走过来,才有了今天。笔者没去过教授编程的学校,也几乎没学什么复杂的理论就开始开发操作系统了。但也正是因为这样,笔者做出的操作系统与其他的操作系统大不相同,非常有个性,所以得到了专家们的一致好评,而且现在还能有机会写这本书,向初学者介绍经验。总地说来,笔者从着手开发直到现在,每天都是乐在其中的。 正是像笔者这样自己摸着石头过河,一路磕磕绊绊走过来的人,讲出的东西才简单易懂。不过在讲解过程中会涉及失败的经验,以及如何重新修正最终取得成功,所以已经懂了的人看着可能会着急。不好意思,如果碰到这种情况请忍耐一下吧。 读了这部分内容或许有人会觉得“是不是什么都不学习才是最好的啊” ,其实那倒不是。比如工作上需要编写某些程序,或者一年之内要完成某些任务,这时没有时间去故意绕远路,所以为了避免不必要的

      11、失败,当然是先学习再着手开发比较好。但这次我们是因为自己的兴趣而学习操作系统的开发的,既然是兴趣,那就是按自己喜欢的方式慢慢来,这样就挺好的。 6 第 0天:着手开发之前 5 如何开发操作系统 操作系统(OS)一般打开电源开关就会自动执行。这是怎么实现的呢?一般在Windows上开发的可执行文件(.exe) ,都要在操作系统启动以后,双击一下才能运行。我们这次想要做的可不是这种可执行程序,而是希望能够做到把含有操作系统的CD-ROM或软盘插入电脑,或者将操作系统装入硬盘后,只要打开电源开关就能自动运行。 为了开发这样的操作系统,我们准备按照如下的步骤来进行。 也就是说,所谓开发操作系统,就是想办法制作一张“含有操作系统的,能够自动启动的磁盘” 。 这里出现的“映像文件”一词,简单地说就是软盘的备份数据。我们想要把特定的内容写入磁盘可不是拿块磁铁来在磁盘上晃晃就可以的。所以我们要先做出备份数据,然后将这些备份数据写入磁盘,这样才能做出符合我们要求的磁盘。 软盘的总容量是1440KB,所以作为备份数据的映像文件也恰好是1440KB。一旦我们掌握了制作磁盘映像的方法,就可以按自己的想法制作任

      12、意内容的磁盘了。 这里希望大家注意的是,开发操作系统时需要利用Windows等其他的操作系统。这是因为我们要使用文本编辑器或者C编译器,就必须使用操作系统。既然是这样,那么世界上第一个操作系统又是怎么做出来的呢?在开发世界上第一个操作系统时,当然还没有任何现成的操作系统可供利用,因此那时候人们不得不对照着CPU的命令代码表,自己将0和1排列起来,然后再把这些数据写入磁盘(估计那个时候还没有磁盘,用的是其他存储设备) 。这是一项非常艰巨的工作。 所以恐怕最初的操作系统功能非常有限, 做好之后人们再利用它来开发一个稍微像点样的操作系统, 然后再用这个来开发更实用的操作系统操作系统应该就是这样一步一步发展过来的。 source program,为了生成机器码所写的程序代码。可通过编译器编译成机器语言。 CPU能够直接理解的语言,由二进制的0和1构成。其实源代码也是由 0和1构成的(后述) 。 5 6 操作系统开发中的困难 7 0 3 20 21 8 10 16 9 22 7 12 11 13 15 14 15 由于这次大部分初学者都是Windows用户,所以决定使用Windows这个现成的操

      13、作系统,Windows95/98/Me/2000/XP中任意一个版本都可以。肯定也有人会说还是Linux好用,所以笔者也总结了一下Linux上的做法,具体内容写在了帮助与支持里,有需要的人请一定看一看。 另外,如果C编译器和映像文件制作工具等不一样的话,开发过程中就会产生一些细微的差别,这很难一一解释,所以笔者就直接把所有的工具都放到附带光盘里了。这些几乎都是笔者所发布的免费软件,它们大都是笔者为了开发后面的OSASK操作系统而根据需要自己编写的。这些工具的源代码也是公开的。除此之外,我们还会用到其他一些免费软件,所有这些软件的功能我们会在使用的时候详细介绍。 6 操作系统开发中的困难 现在市面上众多的C编译器都是以开发Windows或Linux上的应用程序为前提而设计的,几乎从来没有人想过要用它们来开发其他的软件,比如自己的操作系统。笔者所提供的编译器,也是以Windows版的gcc为基础稍加改造而做成的, 与gcc几乎没什么不同。或许也有为开发操作系统而设计的C编译器,不过就算有,恐怕也只有开发操作系统的公司才会买,所以当然会很贵。这次我们用不了这么高价的软件。 因为这些原因,我们

      14、只能靠开发应用程序用的C编译器想方设法编写出一个操作系统来。这实际上是在硬来,所以当中就会有很多不方便的地方。 就比如说printf(“hellon”);吧,这个函数总是出现在C语言教科书的第一章,但我们现在就连它也无法使用。为什么呢?因为printf这个函数是以操作系统提供的功能为前提编写的,而我们最开始的操作系统可是什么功能都没有。因此,如果我们硬要执行这个函数的话,CPU会发生一般保护性异常,直接罢工。刚开始的时候不仅是printf,几乎所有的函数都无法使用。 关于这次开发语言的选择,如果非要说出个所以然的话,其实也是因为C语言还算是很少依赖操作系统功能的语言,基本上只要不用函数就可以了。如果用C+的话,像new/delete这种基本而重要的运算符都不能用了,另外对于类的做法也会有很多要求,这样就无法发挥C+语言的优势了。当然,为了使用这些函数去开发操作系统,只要我们想办法,还是能够克服种种困难的。但是如果做到这个份上,我们不禁会想,到底是在用C+做操作系统呢, http:/hrb.osask.jp。 GNU项目组开发的免费C编译器, GNU C Compiler的简称。 有时也

      15、指GUN开发的各种编译器的集合 (GNU Compiler Collection) 。 电脑的CPU非常优秀,如果接到无视OS保护的指令或不可能执行的指令时,首先会保存当前状态,中断正在执行的程序,然后调用事先设定的函数。这种机制称为异常保护功能,比如除法异常、未定义指令异常、栈异常等。不能归类到任何异常类型中去的异常事态被称为一般保护异常。这种异常保护功能或许会让老Windows用户想起那噩梦般的蓝屏画面,但是如果经历过操作系统开发以后,大家就会觉得这种机制实在是太有用了。 6 8 第 0天:着手开发之前 还是在为了C+而做操作系统呢。对别的语言而言这个问题会更加突出,所以这次还是决定使用C语言,希望大家予以理解。 顺便插一句,在开发操作系统时不会受到限制的语言大概就只有汇编语言了。还是汇编语言最厉害(笑) 。但是如果本书仅用汇编来编写操作系统的话,恐怕没几个人会看,所以就算是做事管前不顾后的笔者也不得不想想后果。 另外,在开发操作系统时,需要用到CPU上的许多控制操作系统的寄存器。一般的C编译器都是用于开发应用程序的,所以根本没有任何操作这些寄存器的命令。另外,C编译器还具有非常优

      16、秀的自动优化功能,但有时候这反而会给我们带来麻烦。 归根到底,为了克服以上这些困难,有些没法用C语言来编写的部分,我们就只好用汇编语言来写了。这个时候,我们就必须要知道C编译器到底是怎样把程序编译成机器语言的。如果不能够与C编译器保持一致的话,就不能将汇编语言编写的部分与C语言编写的部分很好地衔接起来。这可是在编写普通的C语言程序时所体会不到哦!不过相比之下,今后的麻烦可比这种好处多得多啊(苦笑) 。 同样,如果用C+来编写操作系统,也必须知道C+是如何把程序编译成机器语言的。当然,C+比C功能更多更强,编译规则也更复杂,所以解释起来也更麻烦,我们选用C语言也有这一层理由。总之,如果不理解自己所使用的语言是如何进行编译的,就没法用这种语言来编写操作系统。 书店里有不少C语言、C+的书,当然也还有Delphi、Java等其他各种编程语言的书,但这么多书里没有一本提到过“这些源代码编译过后生成的机器语言到底是什么样的” 。不仅如此,虽然我们是在通过程序向CPU发指令的,但连CPU的基本结构都没有人肯给我们讲一讲。作为一个研究操作系统的人, 真觉得心里不是滋味。 为了弥补这一空缺, 我们这本

      17、书就从这些基础讲起 (但也仅限于此次开发操作系统所必备的基础知识) 。 我们具备了这样的知识以后,说不定还会改变对程序设计的看法。以前也许只想着怎么写出漂亮的源代码来,以后也许就会更注重编译出来的是怎样的机器语言。源代码写得再漂亮,如果不能编译成自己希望的机器语言,不能正常运行的话,也是毫无意义的。反过来说,即便源代码写得难看点儿,即便只有特定的C编译器才能编译,但只要能够得到自己想要的机器语言就没有问题了。虽然不至于说“只要编译出了想要的机器语言,源代码就成了一张废纸” ,但从某种意 Assembler,与机器语言最接近的一种编程语言。过去掌握这种语言的人会备受尊敬,而现在这种人恐怕要被当作怪人了,真是可悲啊。原本汇编语言的正式名称应该是Assembly语言,而Assembler一般指的是编译程序。不过像笔者这样的老程序员,往往不对这两个词进行区分,统称为Assembler。 读到这里,大家可能还不理解为什么这么说,越往后看就越能慢慢体会到了。 Register,有些类似机器语言中的变量。对CPU而言,内存是外部存储装置,在CPU内核之中,存储装置只有寄存器。全部寄存器的容量加起来也

      18、不到1KB。 7 学习本书时的注意事项(重要! ) 9 0 3 20 21 8 10 16 9 22 7 12 11 13 15 14 15 义上说还真就是这样。 对于开发操作系统的人而言,源程序无非是用来得到机器语言的“手段” ,而不是目的。浪费太多时间在手段上就是本末倒置了。 对了,还有一点或许会有人担心,所以在这里事先说明一下:虽然操作系统是用C语言和汇编语言编写的,但并不是用C+编写的应用程序就无法在这个操作系统上运行。编写应用程序所用的语言,与开发操作系统所使用的语言是没有任何关系的,大家大可不必担心。 7 学习本书时的注意事项(重要! ) 本书从第1章开始,写的是每一天实际开发的内容,虽然一共分成了30天,但这些都是根据笔者现在的能力和讲解的长度来大概切分的,并不是说读者也必须得一天完成一章。每个人觉得难的地方各不相同,有时学习一章可能要花上一星期的时间,也有时可能一天就能学会三章的内容。 当然,学习过程中可能会遇到看不太懂的章节,这种时候不要停下来,先接着往下读上个一两章也许会突然明白过来。如果往后看还是不明白的话,就先确认一下自己已经理解到哪一部分了,然后回过头来再从不

      19、懂的地方重新看就是了。千万别着急,看第二遍时,没准就会豁然开朗了。 如果已经弄清了哪里没理解,而且没理解的部分看了很多遍还是不明白的话,大家可以参阅我们的帮助与支持页面,或许“问题与解答” (Q&A)页里会有解说。 本书对C语言的指针和结构体的说明与其他书籍有很大区别。这是因为本书先讲CPU的基本结构,然后讲汇编,最后再讲C语言,而其他的书都不讲这些基础知识,刚一提到指针,马上就转到变量地址如何如何了。所以就算大家“觉得”已经明白了那些书里讲的指针,也不要把本书的指针部分跳过去,相信这次大家能真正地理解指针。当然,如果真的已经弄明白了的话,大概看看就可以了。 从现在开始我们来一点一点地开发操作系统,我们会将每个阶段的进展情况总结出来,这些中间成果都刻在附带光盘里了,只要简单地复制一下就能马上运行。关于这些程序,有些需要注意的地方,我们在这里简单说明一下。 比如最初出现的程序是“helloos0” ,下一个出现的程序是“helloos1” 。 即使我们以helloos0 http:/hrb.osask.jp。 7 10 第 0天:着手开发之前 为基础,把书中讲解的内容一个不漏地全部做上一

      20、遍,也不能保证肯定可以得到后面的helloos1。书中可能偶尔有讲解得很完整的地方,但其实大多部分都讲得不够明确,这主要是因为笔者觉得这些地方不讲那么仔细大家肯定也能明白。 笔者说这些主要就是想要告诉大家,不仅要看书里的内容,更要好好看程序。有时候书上写得很含糊,读起来晦涩难懂,但一看程序马上就明白了。本书的主角不是正文内容,而是附录中的程序。正文仅仅是介绍程序是如何做出来的。 所以说从这个意义上讲,与其说这是“一本附带光盘的书” ,倒不如说这是“一张附带一本大厚书的光盘” (笑) 。 关于程序还有一点要说明的这里收录的程序的版权全部归笔者所有。可是,读了这本书后打算开发自己的操作系统的话,可能有不少地方要仿照着附带程序来做;也有人可能想把程序的前期部分全盘照搬过来用;还有人可能想接着本书最后的部分继续开发自己的操作系统。 这是一本关于操作系统的教材,如果大家有上面这些想法却不能自由使用附录程序的话,这教材也就没什么意义了,所以大家可以随意使用这些程序,也不用事先提出任何申请。尽管大家最后做出来的操作系统中可能会包含笔者编写的程序,不过也不用在版权声明中署上笔者的名字。大家可以把它当作

      21、自己独立开发的操作系统,也可以卖了它去赚钱。就算大家靠这个系统成了亿万富翁,笔者也不会要分毫的分成,大家大可放心。 而且这不只是买了本书的人才能享受的特权,从图书馆或朋友那儿借书看的人,甚至在书店里站着只看不买的人,也都享有以上权利。当然,大家要是买了这本书,对笔者、对出版社都是一个帮助。 (笑) 在引用本书程序时,只有一点需要注意,那就是大家开发的操作系统的名字。因为它已经不是笔者所开发的操作系统了,所以请适当地改个名字,以免让人误解,仅此一点请务必留意。不管程序的内部是多么相像,它都是大家自己负责发布的另外一个不同的操作系统。给它起个响亮的名字吧。 以上声明仅适用于书中的程序,以及附带光盘中收录的用作操作系统教材的程序。本书正文和附带光盘中的其他工具软件不在此列。复制或修改都受到著作权法的保护。请在法律允许范围内使用这些内容。与光盘中的工具软件相关的许可权会放在本书最后一章予以说明。 在版权署名时,如果有人执意要署上笔者的名字,笔者也不反对。另外,要是大家一不小心发了大财,一定要给笔者分红的话,笔者当然也会心存感激地接受下来(笑) 。 8 各章内容摘要 11 0 3 20 21 8

      22、 10 16 9 22 7 12 11 13 15 14 15 8 各章内容摘要 估计看过目录大家就能大概了解各章内容了,但因为目录里项目太多,所以在这里概括总结一下。如果有人想要保留一份神秘感,想边看边猜“后面的内容会是什么” ,那么可以跳过本节不读(笑) 。这一部分可以说是全书的灯塔,当大家在阅读本书的过程中感觉有什么不放心的时候,就回过头来重新看看本节内容吧。 第一周(第1天 第7天) 一开始首先要考虑怎么来写一个 “只要一通电就能运行的程序” 。 这部分用C语言写起来有些困难,所以主要还是用汇编语言来写。 这步完成之后,下一步就要写一个从磁盘读取操作系统的程序。这时即便打开电脑电源,它也不会自动地将操作系统全部都读进来,它只能读取磁盘上最开始的512字节的内容,所以我们要编写剩余部分的载入程序。这个程序也要用汇编语言编写。 一旦完成了这一步,以后的程序就可以用C语言来编写了。我们就尽快使用C语言来学习开发显示画面的程序。同时,我们也能慢慢熟悉C语言语法。这个时候我们好像在做自己想做的事,但事实上我们还没有自由操纵C语言。 接下来,为了实现“移动鼠标”这一雄心,我们要对CPU进行

      23、细致的设定,并掌握中断处理程序的写法。从全书总体看来,这一部分是水平相当高的部分,笔者也觉得放在这里有些不妥,但从本书条理上讲,这些内容必须放在这里,所以只好请大家忍耐一下了。在这里,CPU的规格以及电脑复杂的规格都会给我们带来各种各样的麻烦。 而且开发语言既有C语言, 又有汇编语言,这又给我们造成了更大的混乱。这个时候我们一点儿也不会觉得这是在做自己想做的事,怎么看都像是在“受人摆布” 。 渡过这个痛苦的时期,第一周就该结束了。 第二周(第8天 第14天) 一周的苦战还是很有意义的,回头一看,我们就会发现自己还是斩获颇丰的。这时我们已经基本掌握了C语言的语法,连汇编语言的水平也能达到本书的要求了。 所以现在我们就可以着手开发像样的操作系统了。但是这一次我们又要为算法头痛了。即使掌握了编程语言的语法,如果不懂得好的算法的话,也还是不能开发出来自己想要的操作系统。所以这一周我们就边学习算法边慢慢地开发操作系统。不过到了这一阶段,我们就能感觉到基本上不会再受技术问题限制了。 8 12 第 0天:着手开发之前 第三周(第15天 第21天) 现在我们的技术已经相当厉害了,可以随心所欲地开发自己

      24、的操作系统了。首先是要支持多任务,然后是开发命令行窗口,之后就可以着手开发应用程序了。到本周结束时,就算还不够完备,我们也能拿出一个可以称之为操作系统的软件了。 第四周(第22天 第28天) 在这个阶段,我们可以尽情地给操作系统增加各种各样的功能,同时还可以开发出大量像模像样的应用程序来。这个阶段我们已经能做得很好了,这可能也是我们最高兴的时期。这部分要讲解的内容很少,笔者也不用再煞费苦心地去写那些文字说明了,可以把精力都集中在编程上(笑) 。对了,说起文字才想起来,正好在这个时期可以让我们的操作系统显示文字了。 免费赠送两天(第29天 第30天) 剩下的两天用来润色加工。这两天我们来做一些之前没来得及做,但做起来既简单又有趣的内容。 以上就是从第1天到第30天的内容摘要,越到后面介绍越短,这也说明最开始的内容是最复杂的。那么,就让我们做好准备,开始第一天的学习吧。啊,大家不用紧张,放松!放松! 从计算机结构到汇编程序入门 先动手操作 究竟做了些什么 初次体验汇编程序 加工润色 1 先动手操作 与其啰啰嗦嗦地写上一大堆,还不如实际动手开发来得轻松,我们这就开始吧。而且我们一上来就完全抛

      25、开前面的说明,既不用C语言,也不用汇编程序,而是采用一个迥然不同的工具来进行开发(笑) 。 有一种工具软件名为“二进制编辑器” (Binary Editor),是一种能够直接对二进制数进行编辑的软件。我们现在要用它来编辑出下图这样的文件。 也许有人会说“这样的工具我从来没有见过呀” ,没关系,下面我们来详细地介绍一下。 首先打开下面这个网页: http:/www.zob.ne.jp/c.mos/soft/bz.html 原文直译为“二进制编辑器” (Binary Editor) ,在中国“二进制编辑器” 、 “十六进制编辑器”这两种说法都有,这里尊重原著保留了“二进制编辑器”的说法。译者注 如果此网页连接不上,也可用google等检索工具来搜索一下,从别处下载Bz1621.lzh。 第 1 天 1 14 第 1天:从计算机结构到汇编程序入门 用BZ打开helloos.img时的画面 点击“在此下载” (Download)的链接,下载文件Bz1621.lzh (在此非常感谢c.mos公司无偿公开这么好的软件) 。当你读到本书的时候,也许会有新的版本发布,所以文件名可能会有所不同。接下来,

      26、安装下载下来的文件,然后双击启动Bz.exe程序。如果不能正常启动的话,可以参考上面网页的“注意”一项,按照上面的安装指导进行操作。 顺利启动的话屏幕上会出现如下画面。 BZ起动时的画面 好,让我们赶紧来输入吧,只要从键盘上直接输入EB4E904845就可以了,简单吧。其中字符之间的空格是这个软件在显示时为方便阅读自动插入的,不用自己从键盘上输入。另外,右边的.N.HELLOIPL部分,也不用从键盘输入,这是软件自动显示的。可能版本或者显示模式不一样的时候,右侧显示的内容会与下面的截图有所不同。不过不用往心里去,这些内容完全是锦上添花的东西,即使不一样也没事。 输入到000037位置时的画面 1 先动手操作 15 1 1 3 20 21 8 10 16 9 22 7 12 11 13 15 14 从000090开始后面全都是00,一直输入到最后168000这个地址。如果一直按着键盘上的“0”不放手的话,画面上的0就会不停地增加,但因为个数相当多,也还是挺花时间的。如果家里有只猫的话, 倒是可以考虑请它来帮忙按住这个键 (日本的谚语: 想让猫来搭把手, 形容人手不足,连猫爪子都想借用一下

      27、) ,或者也可以干脆就用透明胶把这个键粘上。 168000附近的画面 因为一下子输入到最后实在是挺花时间的,大家也许想保存一下中间结果,这时可以从菜单上选择“文件” (File)“另存为” (Save As) ,画面上就会弹出保存文件的对话框。我们可以随便取个名字进行保存,笔者推荐使用“helloos.img” 。当想要打开保存过的文件时,首先要启动Bz.exe,从菜单上选择“文件” (File)“打开” (Open) ,然后选择目标文件,这样原来保存的内容就能显示出来了。可是这个时候不管我们怎么努力按键盘,它都一点反应也没有。这是怎么回事?难道必须要一次性输入到最后吗?这个大家不必担心,其实只要从菜单里选择“编辑”(Edit)“只读” (Read Only)就可以进入编辑状态啦。好了,我们继续输入。 如果家里的猫自由散漫惯了,不肯帮忙,而大家又不想用透明胶粘键盘这种土方法的话,不妨这样:用鼠标选择一部分0,然后从菜单选择“编辑” (Edit)“复制” (Copy) 。简简单单复制粘贴几次就可以大功告成了,这工具还真方便呀。 哦,对了,差点忘记一件重要的事在地址0001F0和00140

      28、0附近还有些地方不全是00,要像下图那样把它们也改过来,然后整体检查一下,确认没有输入错误。 0001F0附近 16 第 1天:从计算机结构到汇编程序入门 001400附近 下面,我们把输入的内容保存下来就完成了软盘映像文件的制作,这时查看一下文件属性,应该能看到文件大小正好是1474560字节(=14401024字节) 。然后我们将这个文件写入软盘(具体后述) ,并用它来启动电脑。如下所示,画面上会显示出“hello, world”这个字符串。目前的程序虽然简单,但毕竟一打开电脑它就能够自动启动,还能在屏幕上显示出一句话来,已经小小成功了哦。不过,我们现在还没有结束这个程序的方法,所以想要结束的时候,只能把软盘取出来后切断电脑电源,或者重新启动。 至于最关键的往磁盘上写映像文件的方法,笔者已经预先准备好了一个程序。在介绍它的使用方法之前,我们先把笔者准备的工具全都安装进来吧,这样后面讲解起来比较省事。下面我们就来看怎么安装这些工具。 打开附带光盘,里面有一个名为tolset的文件夹,把这个文件夹复制到硬盘的任意一个位置上。现在里面的东西还不多,只有3MB左右,不过以后我们自己开发的软

      29、件也都要放到这个文件夹里, 所以往后它会越来越大, 因此硬盘上最好留出100MB左右的剩余空间。 工具安装到此结束,我们既不用修改注册表,也不用设定路径参数,就这么简单。而且以后不管什么时候,都可以把这整个文件夹移动到任何其他地方。用这些工具,我们不仅可以开发操作系统,还可以开发简单的Windows应用程序或OSASK应用程序等。 tool set的缩写, “工具套件”的意思。 1 先动手操作 17 1 1 3 20 21 8 10 16 9 22 7 12 11 13 15 14 接下来我们打开刚才安装的tolset文件夹, 在文件夹的名字上单击鼠标右键, 从弹出的菜单上选择 “新建” (New) “文件夹” (Folder) 。 画面上会显示出缺省的文件夹名 “新建文件夹” (New Folder) ,我们要把它改为“helloos0” ,并把前面保存的映像文件helloos.img复制到这个文件夹里。另外,刚才安装的tolset文件夹下有个名为z_new_w的子文件夹,其中有!cons_9x.bat和!cons_nt.bat这两个文件,要把它们也复制粘贴到helloos0文件夹里

      30、。 接着,在文件夹helloos0里单击鼠标右键,从弹出的菜单中选择“新建” (New)“文本文件” (Text Document) ,并将文件命名为“run.bat” ,回车后屏幕上会显示“如果改变文件扩展名,可能会导致文件不可用。确实要更改吗?”的对话框,我们选择“是” ,创建run.bat文件。然后在run.bat文件名上单击鼠标右键,在弹出的菜单上选择“编辑” (Edit) ,输入下面内容并保存。 run.bat copy helloos.img ./z_tools/qemu/fdimage0.bin ./z_tools/make.exe -C ./z_tools/qemu 然后按照同样的步骤,创建install.bat,并将下列内容输入进去。 install.bat . /z_tools/ w a: helloos.img 其实以上步骤创建的所有文件都已经给事先给大家准备好了,就放在附带光盘中名为projects01_dayhelloos0的子文件夹里。所以大家只要把光盘上的helloos0复制下来,粘帖到硬盘的tolset文件夹里,所有的准备工作就瞬间完成了。 好了,现在我

      31、们就来把这个有点像操作系统的软件安装到软盘上吧。随便从附近的小店里买片新软盘来, 在Windows下格式化一下 (格式化方法: 把软盘插入磁盘驱动器后打开 “我的电脑” ,在“3.5吋软盘” (3.5inches Floppy)A:上单击鼠标右键,再选择“格式化” (Format)即可) 。对了, 这个时候不要选择 “快速格式化” 选项。 然后用鼠标左键双击helloos0文件夹里的 !cons_nt.bat文件(Windows95/98/Me的用户需要双击!cons_9x.bat) ,屏幕上就会出现一个命令行窗口(console) 。我们先仔细确认一下软盘是否已经插好,然后在命令行窗口上输入“install”并回车,这样安装操作就开始了。稍候片刻,等安装程序执行完毕,我们的操作系统启动盘也就做好了。完成安装之后,也可以关闭刚才的命令行窗口了。 现在我们就用这张操作系统启动软盘来启动一下电脑试试吧,肯定跟刚才一样,会显示出“hello, world”的字样来。 在这里要提醒大家几点:一是软盘虽然不要求必须用全新的,但如果太旧的话,在读写过程18 第 1天:从计算机结构到汇编程序入门 中

      32、容易出问题,所以最好还是不要用太旧的软盘。另外,就算是新盘,如果太便宜的话有时也用不了, 若是发现有问题, 就需要再去买一张。 最后一点, 一旦格式化或者往软盘内安装操作系统,就会把里面原有的东西全部覆盖掉,所以大家千万不要用存有重要文件的软盘来尝试哦。 看到这里,大家可能会有各种问题: “这些我都明白,可是既要专门去买张软盘,又要重启电脑,实在太麻烦了,难道就没有什么更简单的方法吗?” 、 “我家的电脑根本就没有软驱呀” 、“我的电脑没有什么重启按钮,也没有关电源的开关,一旦启动了这个奇怪的操作系统,就没法终止啦” 。其实这些问题笔者已经考虑到了,所以特意准备了一个模拟器。我们有了这个模拟器,不用软盘,也不用终止Windows,就可以确认所开发的操作系统启动以后的动作,很方便呢。 使用模拟器的方法也非常简单,我们只需要在用!cons_nt.bat(或者是!cons_9x.bat)打开的命令行窗口中输入“run”指令就可以了。然后一个名叫QEMU的非常优秀的免费PC模拟器就会自动运行。QEMU不是笔者开发的,它是由国外的一些天才们开发出来的。感谢他们! “我按照你说的一步一步地做了一遍

      33、,可是不行呀!怎么回事呢?”会遇到这种情况的人肯定是个非常认真的人,可能真的完全按照上面步骤用二进制编辑器自己做了一个helloos.img文件出来。出现这种问题,肯定是因为文件中有输入错误的地方,虽然笔者不知道具体错在哪儿,不过建议最好检查一下000000到000090,以及0001F0前后的数据。如果还是不行的话,那就干脆用附带光盘中笔者做的helloos.img好了。 可能有些人嫌麻烦,懒得自己输入,上来就直接使用光盘里的helloos.img文件,这当然也没什么不可以;但笔者认为这种体验(一点一点地输入,再千辛万苦地纠错,最终功夫不负有心人取得成功)本身更难能可贵,建议大家最好还是亲自尝试一下。 就这样, 我们没有去改造现成的操作系统, 而是从零开始开发了一个, 并让它运转了起来 (当然,如果别人承认这是个操作系统的话) 。这太了不起了!大家完全可以在朋友们面前炫耀一番了。仅学习了几个小时开发的一个初学者,就能从零开始做出一个操作系统,这本书不错吧(笑)?这次我们考虑到从键盘直接输入比较麻烦,所以就只让它显示了一条消息;如果能再多输入一些内容的话,那仅用这种方法就可以开发任意一

      34、个操作系统(当然最大只能到1440KB) 。现在唯一的问题是,我们还不知道之前输入的那些“EB 4E 90 48 45”到底是什么意思(而这也正是我们所面临的最大问题) 。今天剩下的时间,以及以后的29天时间里,我们都会讲解这个问题。 2 究竟做了些什么 19 1 1 3 20 21 8 10 16 9 22 7 12 11 13 15 14 2 究竟做了些什么 为什么用这种方法就能开发出操作系统来呢?现在搞清楚这个问题, 会对我们今后的理解很有帮助,所以在这里要稍做说明。 首先我们要了解电脑的结构。电脑的处理中心是CPU,即“central process unit”的缩写,翻译成中文就是“中央处理单元” ,顾名思义,它就是处理中心。如果我们把别的元件当作中心来使用的话,那它就叫做CPU了,所以无论什么时候CPU都总是处理中心。不过这个CPU除了与别的电路进行电信号交换以外什么都不会,而且对于电信号,它也只能理解开(ON)和关(OFF)这两种状态,真是个没用的人呀(虽然它不是人吧,大家领会精神) 。 CPU 我们平时会用电脑写文章、听音乐、修照片以及做其他各种各样的事情,我们用电脑所

      35、做的这些,其实本质上都不过是在与CPU交换电信号而已,而且电信号只有开(ON)和关(OFF)这两种状态。再说直白一点,CPU根本无法理解文章的内容,更不会鉴赏音乐、照片,它只会机械地进行电信号的转换。CPU有计算指令,所以它能够进行整数的加减乘除运算,也可以处理负数、计算小数以及10的100次方这样庞大的数值,它甚至能够处理我们初中才学到的平方根和高中才学到的对数、三角函数,而且所有这些计算仅通过一条指令就能简单实现。虽然CPU功能如此强大,但它其实根本不理解数的概念。CPU就是个集成电路板,它只是忠实地执行电信号给它的指令,输出相应的电信号。 这些概念可能不太容易理解,还是让我们来看个的具体例子吧。比如说,让我们用1来表示开 (ON) , 用0来表示关 (OFF) , 这样比较容易理解。 我们可以用3216=512个开 (ON) 和关 (OFF)的集合(电信号的集合) ,来显示出下面这个不甚好看的人头像。 2 20 第 1天:从计算机结构到汇编程序入门 我们也可以用0000 0000 0000 0000 0000 0100 1010 0010 这32个电信号的集合来表示1186这个整

      36、数。 (注: 用二进制表示1186的话, 就是100 1010 0010) 。 我们还可以用0100 1011 0100 1111 0100 1111 0100 0010这32个电信号的集合来表示“BOOK”这个单词(注:这实际上就是电脑内部保存这个单词时的电信号集合) 。 CPU能看见的就只有这些开(ON)和关(OFF)的电信号。换句话说,假如我们给CPU发送这么一串电信号: 0000 0100 0011 1000 0000 1110 0001 0000 这信号可能是一幅画的部分数据,可能是个二进制整数,可能是一段音乐旋律,可能是文章中的一段文字, 也可能是保存了的游戏的一部分数据, 或者是程序中的一行代码, 不管它是什么,CPU都一窍不通。CPU不懂这些,也不在乎这些,它只是默默地、任劳任怨地按照程序的指令进行相应的处理。 看到这里,或许有人会认为是先有了这么多要做的事情,所以人类才发明了CPU,而实际上并不是这样。最早人们发明CPU只是为了处理电信号,那个时候没有人能想到它后来会成为这么有用的机器。不过后来人们发现,一旦把电信号的开(ON)/关(OFF)与数字0和1对应起来,就能

      37、将二进制数转换为电信号,同时电信号也可以转换回二进制数。所以,虽然CPU依然只能处理电信号,但它从此摇身一变,成了神奇的二进制数计算机。 因为我们可以把十进制数转换成二进制数,也能把二进制数还原成十进制数,所以人们又发明了普通的计算机。后来,我们发现只要给每个文字都编上号(即文字编码) ,就可以建立一个文字与数字的对应关系,从而就可以把文字也转换成电信号,让CPU来处理文章(比如进行文字输入或者字词检索等) 。依此类推,人们接着又找到了将图像、音乐等等转换成电信号的方法,使CPU的应用范围越来越广。不过CPU还是一如既往,只能处理电信号。 而且我们能用CPU来处理的并不仅仅只有数据,我们还可以用电信号向CPU发出指令。其实我们所编写的程序最终都要转换成所谓的机器语言,这些机器语言就是以电信号的形式发送给CPU的。这些机器语言不过就是一连串的指令代码,实际上也就是一串0和1的组合而已。 软盘的原理也有异曲同工之妙,简单说来,就是把二进制的0和1转换为磁极的N极和S极而已,所以我们只用0和1就可以写出映像文件来。不仅是映像文件,计算机所能处理的各种文件最终都是用0和1写成的。因此可以说,不

      38、能仅用0和1来表达的内容,都不能以电信号的形式传递给CPU,所以这种内容是计算机所无法处理的。 而“二进制编辑器”就是用来编辑二进制数的,我们可以很方便地用它来输入二进制数,并2 究竟做了些什么 21 1 1 3 20 21 8 10 16 9 22 7 12 11 13 15 14 保存成文件。所以它就是我们的秘密武器,也就是说只要有了二进制编辑器,随便什么文件我们都能做出来。 (厉害吧! )如果大家在商店里看到一个软件,很想要而又不想花那么多钱的话,那就干脆就回家用二进制编辑器自己做一个算啦! 用这个方法我们完全可以自己制作出一个与店里商品一模一样的东西来。看上一个500万像素的数码相机,但是太贵了买不起?那有什么关系?我们只要有二进制编辑器在手,就可以制作出毫不逊色于相机拍摄效果的图像,而且想做几张就可以做几张。要是C编译器太贵了买不起,也不用郁闷。即使没有C编译器,我们也可以用二进制编辑器做出一个与编译器生成文件完全一样的执行文件,而且就连C编译器本身都可以用二进制编辑器做出来。 有了这么强大的工具,制作操作系统就是小菜一碟。道理就是这么简单,所以我们这次不费吹灰之力就做了个操

      39、作系统出来也是理所当然的。或许有人会想“就为了讲这么个小事,有必要长篇大论写这么多吗?”其实不然,如果我们对CPU的基础有了彻底的理解,以后的内容就好懂多了。 “喂,且慢,我明白了二进制编辑器就是编辑二进制数的软件,可是在你让我输入你的helloos.img的时候,除了0和1以外,不是还让我输入了很多别的东西吗?你看,第一个不就是E吗?这哪里是什么二进制数?分明是个英文字母嘛! ”噢,不好意思,这说得一点错都没有。 虽然二进制数与电信号有很好的一一对应关系,但它有一个缺点,那就是位数实在太多了,举个例子来说,如果我们把1234写成二进制数,就成了10011010010,居然长达11位。而写成十进制数,只用4位就够了。因为这样也太浪费纸张了,所以计算机业界普遍使用十六进制数。十进制数的1234写成十六进制数,就是4D2,只用3位就够了。 那为什么非要用十六进制数呢,用十进制数不是也挺好的吗?实际上,我们可以非常简便地把二进制数写成十六进制数。 二进制数和十六进制数对照表 0000 0 0100 4 1000 8 1100 C 0001 1 0101 5 1001 9 1101 D 001

      40、0 2 0110 6 1010 A 1110 E 0011 3 0111 7 1011 B 1111 F 有了这个对照表,我们就能轻松进行二进制与十六进制之间的转换了。将二进制转换为十六进制时,只要从二进制数的最后一位开始,4位4位地替换过来就行了。如: 100 1101 0010 4D2 反之,把十六进制数的4D2转换为二进制数的100 1101 0010也很简单,只要用上面的对照表反过来变换一下就行了。而十进制数变换起来就没这么简单了。同理,八进制数是把3位一组的22 第 1天:从计算机结构到汇编程序入门 二进制数作为一个八进制位来变换的,这种计数法在计算机业界也偶有使用。 因此我们在输入EB的时候,实际上是在输入11101011,所以它其实是个十六进制编辑器,但笔者习惯称它为二进制编辑器,希望大家不要见怪。 虽然笔者对二进制编辑器如此地赞不绝口,但用它也解决不了什么实际问题。因为这就相当于“只要有了笔和纸,什么优秀的小说都能写出来”一样。笔和纸不过就是笔和纸而已,实际上对创作优秀的小说也帮不上多大的忙。所以大家在写程序时,用的都是文本编辑器和编译器,没有谁只用二进制编辑器来做程序

      41、的。大家照相用的也都是数码照相机,没有谁只用二进制编辑器来做图像文件。因此,我们用二进制编辑器进行的开发就到此为止,接下来我们要调转方向,开始用编程语言来继续我们的开发工作。不过有了这次的经验,我们就知道了如果今后遇到什么特殊情况还可以使用二进制编辑器,它是非常有用的。而且后面章节中我们偶尔也会用到它。 3 初次体验汇编程序 好, 现在就让我们马上来写一个汇编程序, 用它来生成一个跟刚才完全一样的helloos.img吧。我们这次使用的汇编语言编译器是笔者自己开发的,名为“nask” ,其中的很多语法都模仿了自由软件里享有盛名的汇编器“NASM” ,不过在“NASM”的基础之上又提高了自动优化能力。 超长的源代码 DB 0xeb, 0x4e, 0x90, 0x48, 0x45, 0x4c, 0x4c, 0x4f DB 0x49, 0x50, 0x4c, 0x00, 0x02, 0x01, 0x01, 0x00 DB 0x02, 0xe0, 0x00, 0x40, 0x0b, 0xf0, 0x09, 0x00 DB 0x12, 0x00, 0x02, 0x00, 0x00, 0x00,

      42、0x00, 0x00 DB 0x40, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x29, 0xff (为节省纸张,这里省略中间的18万4314行) DB 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 我们使用复制粘帖的方法, 就可以写出这样一个超长的源代码来, 将其命名为 “helloos.nas” ,并保存在helloos0中。仔细看一下就能发现这个文件内容与我们用二进制编辑器输入的内容是一模一样的。 接着,我们用“!cons_nt.bat”或是“!cons_9x.bat” (我们在前面已经说过,要根据Windows的版本决定用哪一个。 以后每次都这样解释一遍的话比较麻烦, 所以我们就将它简写为!cons好了) 打开一个命令行窗口(console) ,输入以下指令(提示符部分不用输入) : 提示符.z_toolsnask.exe helloos.nas helloos.img prompt,出现在命令行窗口中,提示用户进行输入的信息。 3 3 初次体验汇编程序 23 1 1 3 20 21 8 10 16 9 22

      43、 7 12 11 13 15 14 这样我们就得到了映像文件helloos.img。 好,我们的第一个汇编语言程序就这样做成了!不过这么写程序也太麻烦了,要做个18万行的程序, 不但浪费时间, 还浪费硬盘空间。 与其这样还不如用二进制编辑器呢, 不用输入 “0x” 、“,”什么的,还能轻松一点。 其实要解决这个问题并不难,如果我们不只使用DB指令,而把RESB指令也用上的话,就可以一下将helloos.nas缩短了,而且还能保证输出的内容不变,具体我们来看下面。 正常长度的源程序 DB 0xeb, 0x4e, 0x90, 0x48, 0x45, 0x4c, 0x4c, 0x4f DB 0x49, 0x50, 0x4c, 0x00, 0x02, 0x01, 0x01, 0x00 DB 0x02, 0xe0, 0x00, 0x40, 0x0b, 0xf0, 0x09, 0x00 DB 0x12, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 DB 0x40, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x29, 0xff DB 0xff

      44、, 0xff, 0xff, 0x48, 0x45, 0x4c, 0x4c, 0x4f DB 0x2d, 0x4f, 0x53, 0x20, 0x20, 0x20, 0x46, 0x41 DB 0x54, 0x31, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00 RESB 16 DB 0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c DB 0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a DB 0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09 DB 0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb DB 0xee, 0xf4, 0xeb, 0xfd, 0x0a, 0x0a, 0x68, 0x65 DB 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72 DB 0x6c, 0x64, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00

      45、 RESB 368 DB 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00 RESB 4600 DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00 RESB 1469432 我们自己动手输入这段源程序比较麻烦,所以笔者把它放在附带光盘的projects01_day helloos1目录下了。 大家只要把helloos1文件夹复制粘帖到tolset文件夹里就可以了。 之前的helloos0文件夹以后就不用了, 我们可以把它删除, 也可以放在那里留作纪念。 顺便说一下, 笔者将helloos0文件夹名改为了helloos1,删掉了其中没用的文件,新建并编辑了需要用到的文件,这样就做出了新的helloos1文件夹。操作系统就是这样一点一点地成长起来的。 每次进行汇编编译的时候,我们都要输入刚才的指令,这太麻烦了,所以笔者就做了一个批处理文件asm.bat。 有了这个批处理文件, 我们只要在用 “!con

      46、s” 打开的命令行窗口里输入 “asm” , batch file,基本上只是将命令行窗口里输入的命令写入文本文件。虽然还有功能更强的处理,但本书中我们用不到。所谓批处理就是批量处理,即一次处理一连串的命令。 24 第 1天:从计算机结构到汇编程序入门 就可以生成helloos.img文件。在用“asm”作成img文件后,再执行“run”指令,就可以得到与刚才一样的结果。 DB指令是“data byte”的缩写,也就是往文件里直接写入1个字节的指令。笔者喜欢用大写字母来写汇编指令,但小写的“db”也是一样的。 在汇编语言的世界里,这个指令是程序员的杀手锏,也就是说只要有了DB指令,我们就可以用它做出任何数据(甚至是程序) 。所以可以说,没有用汇编语言做不出来的文件。文本文件也好,图像文件也好,只要能叫上名的文件,我们都能用汇编语言写出来。而其他的语言(比如C语言)就没有这么万能。 RESB指令是“reserve byte”的略写,如果想要从现在的地址开始空出10个字节来,就可以写成RESB 10,意思是我们预约了这10个字节(大家可以想象成在对号入座的火车里,预订了10个连号座位的情形

      47、) 。而且nask不仅仅是把指定的地址空出来,它还会在空出来的地址上自动填入0x00,所以我们这次用这个指令就可以输出很多的0x00,省得我们自己去写18万行程序了,真是帮了个大忙。 这里还要说一下,数字的前面加上0x,就成了十六进制数,不加0x,就是十进制数。这一点跟C语言是一样的。 4 加工润色 刚才我们把程序变成了短短的22行,这成果令人欣喜。不过还有一点不足就是很难看出这些程序是干什么的,所以我们下面就来稍微改写一下,让别人也能看懂。改写后的源文件增加到了48行,它位于附带光盘的projects01_dayhelloos2目录下,大家可以直接把helloos2文件夹复制到tolset里。现在helloos1也可以删掉了(每个文件夹都是独立的,用完之后就可以删除,以后不再赘述。当然放在那里留作纪念也是可以的) 。 现在的程序有50行,也占不了多少地方,所以我们将它写在下面了。 有模有样的源代码 ; hello-os ; TAB=4 ; 以下这段是标准FAT12格式软盘专用的代码 DB 0xeb, 0x4e, 0x90 DB HELLOIPL ; 启动区的名称可以是任意的字符串(8

      48、字节) DW 512 ; 每个扇区(sector)的大小(必须为512字节) DB 1 ; 簇(cluster)的大小(必须为1个扇区) 4 4 加工润色 25 1 1 3 20 21 8 10 16 9 22 7 12 11 13 15 14 DW 1 ; FAT的起始位置(一般从第一个扇区开始) DB 2 ; FAT的个数(必须为2) DW 224 ; 根目录的大小(一般设成224项) DW 2880 ; 该磁盘的大小(必须是2880扇区) DB 0xf0 ; 磁盘的种类(必须是0xf0) DW 9 ; FAT的长度(必须是9扇区) DW 18 ; 1个磁道(track)有几个扇区(必须是18) DW 2 ; 磁头数(必须是2) DD 0 ; 不使用分区,必须是0 DD 2880 ; 重写一次磁盘大小 DB 0,0,0x29 ; 意义不明,固定 DD 0xffffffff ;(可能是)卷标号码 DB HELLO-OS ; 磁盘的名称(11字节) DB FAT12 ; 磁盘格式名称(8字节) RESB 18 ; 先空出18字节 ; 程序主体 DB 0xb8, 0x00, 0x00,

      49、0x8e, 0xd0, 0xbc, 0x00, 0x7c DB 0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a DB 0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09 DB 0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb DB 0xee, 0xf4, 0xeb, 0xfd ; 信息显示部分 DB 0x0a, 0x0a ; 2个换行 DB hello, world DB 0x0a ; 换行 DB 0 RESB 0x1fe-$ ; 填写0x00,直到 0x001fe DB 0x55, 0xaa ; 以下是启动区以外部分的输出 DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00 RESB 4600 DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00 RESB 1469432 这里有几点新内容,我们逐一来看一下。首先是“;”命令,这是个注释命令,相当于C语言或是C+

      50、中的“/” 。正是因为有它,我们才可以在源代码里加入很多注释。 其次是DB指令的新用法。我们居然可以直接用它写字符串。在写字符串的时候,汇编语言会自动地查找字符串中每一个字符所对应的编码,然后把它们一个字节一个字节地排列起来。这个功能非常方便,也就是说,当我们想要变更输出信息的时候,就再也不用自己去查字符编码表了。 26 第 1天:从计算机结构到汇编程序入门 再有就是DW指令和DD指令,它们分别是“data word”和“data double-word”的缩写,是DB指令的“堂兄弟” 。word的本意是“单词” ,但在计算机汇编语言的世界里,word指的是“16位”的意思,也就是2个字节。 “double-word”是“32位”的意思,也就是4个字节。 对了,差点忘记说RESB 0x1fe-$了。这个美元符号的意思如果不讲,恐怕谁也搞不明白,它是一个变量,可以告诉我们这一行现在的字节数(如果严格来说,有时候它还会有别的意思,关于这一点我们明天再讲) 。 在这个程序里, 我们已经在前面输出了132字节, 所以这里的$就是132。因此nask先用0x1fe减去132,得出378这一结果,

      51、然后连续输出378个字节的0x00。 那这里我们为什么不直接写378,而非要用$呢?这是因为如果将显示信息从“hello, world”变成“this is a pen.”的话,中间要输出0x00的字节数也会随之变化。换句话说,我们必须保证软盘的第510字节(即第0x1fe字节)开始的地方是55 AA。如果在程序里使用美元符号($)的话,汇编语言会自动计算需要输出多少个00,我们也就可以很轻松地改写输出信息了。 既然可以毫不费力地改写显示的信息,就一定要好好发挥这一功能,让我们的操作系统显示出自己喜欢的一句话,让它成为一个只属于我们自己的、世界上独一无二的操作系统。不过遗憾的是现在它还不能显示汉字。 当然大家也可以尝试一下, 但由于这个程序还没有显示汉字的功能,所以显示出来的都是乱码,因此大家先将就一下,用英语或拼音吧。 最后再给大家解释一下程序中出现的几个专门术语。时间不早了,我们今天就到这吧。其他的留待明天再说。 TAB=4.有的文本编辑器可以调整TAB键的宽度。请使用这种编辑器的人将TAB键的宽度设定成4,这样源程序更容易读。可能有人说,我这里只能用记事本(notepad) ,T

      52、AB键宽度固定为8,想调都没法调。没关系,明天笔者来推荐一个好用的文本编辑器。 FAT12格式.(FAT12 Format)用Windows或MS-DOS格式化出来的软盘就是这种格式。我们的helloos也采用了这种格式,其中容纳了我们开发的操作系统。这个格式兼容性好,在Windows上也能用,而且剩余的磁盘空间还可以用来保存自己喜欢的文件。 启动区.(boot sector)软盘第一个的扇区称为启动区。那么什么是扇区呢?计算机读写软盘的时候,并不是一个字节一个字节地读写的,而是以512字节为一个单位进行读写。因此,软盘的512字节就称为一个扇区。一张软盘的空间共有1440KB,也就是1474560字节,除以512得2880,这也就是说一张软盘共有2880个扇区。那为什么第一个扇区称为启动区呢?那是因为计算机首先从最初一个扇区开始读软盘,然后去检查这个扇区最后2个字节的内容。 4 加工润色 27 1 1 3 20 21 8 10 16 9 22 7 12 11 13 15 14 如果这最后2个字节不是55 AA,计算机会认为这张盘上没有所需的启动程序,就会报一个不能启动的错误。(也许有

      53、人会问为什么一定是55 AA呢?那是当初的设计者随便定的,笔者也没法解释)。如果计算机确认了第一个扇区的最后两个字节正好是55 AA,那它就认为这个扇区的开头是启动程序,并开始执行这个程序。 IPL.initial program loader的缩写。启动程序加载器。启动区只有区区512字节,实际的操作系统不像hello-os这么小,根本装不进去。所以几乎所有的操作系统,都是把加载操作系统本身的程序放在启动区里的。有鉴于此,有时也将启动区称为IPL。但hello-os没有加载程序的功能,所以HELLOIPL这个名字不太顺理成章。如果有人正义感特别强,觉得“这是撒谎造假,万万不能容忍!”,那也可以改成其他的名字。但是必须起一个8字节的名字,如果名字长度不到8字节的话,需要在最后补上空格。 启动.(boot)boot这个词本是长靴(boots)的单数形式。它与计算机的启动有什么关系呢?一般应该将启动称为start的。实际上,boot这个词是bootstrap的缩写,原指靴子上附带的便于拿取的靴带。但自从有了吹牛大王历险记(德国)这个故事以后,bootstrap这个词就有了“自力更生完成任务

      54、”这种意思(大家如果对详情感兴趣,可以在Google上查找,也可以在帮助和支持网页http:/hrb.osask.jp上提问)。而且,磁盘上明明装有操作系统,还要说读入操作系统的程序(即IPL)也放在磁盘里,这就像打开宝物箱的钥匙就在宝物箱里一样,是一种矛盾的说法。这种矛盾的操作系统自动启动机制,被称为bootstrap方式。boot这个说法就来源于此。如果是笔者来命名的话,肯定不会用bootstrap 这么奇怪的名字,笔者大概会叫它“多级火箭式”吧。 多任务(1) 挑战任务切换(harib12a) 任务切换进阶(harib12b) 做个简单的多任务(1) (harib12c) 做个简单的多任务(2) (harib12d) 提高运行速度(harib12e) 测试运行速度(harib12f) 多任务进阶(harib12g) 1 挑战任务切换(harib12a) “话说,多任务到底是啥呢?”我们今天的内容,就从这个问题开始吧。 多任务, 在英语中叫做 “multitask” , 顾名思义就是 “多个任务” 的意思。 简单地说, 在Windows等操作系统中,多个应用程序同时运行的状态(也就

      55、是同时打开好几个窗口的状态)就叫做多任务。 对于生活在现代社会的各位来说,这种多任务简直是理所当然的事情。比如你会一边用音乐播放软件听音乐一边写邮件,邮件写到一半忽然有点东西要查,便打开Web浏览器上网搜索。这对于大家来说这些都是家常便饭了吧。 可如果没有多任务的话会怎么样呢?想写邮件的时候就必须关掉正在播放的音乐,要查东西的时候就必须先保存写到一半的邮件,然后才能打开Web浏览器光想象一下就会觉得太不方便了。 然而在从前,没有多任务反倒是普遍的情形(那个时候大家不用电脑听音乐,也没有互联网) 。在那个年代,电脑一次只能运行一个程序,如果要同时运行多个程序的话,就得买好几台电脑才行。 就在那个时候,诞生了最初的多任务操作系统,大家都觉得太了不起了。从现在开始,我们也要准备给“纸娃娃系统”添加执行多任务的能力了。连这样一个小不点儿操作系统都能够实现多任务,真是让人不由地感叹它生逢其时呀。 第 15 天 1 1 挑战任务切换(harib12a) 283 1 2 3 4 5 8 10 15 9 6 7 12 11 13 14 14 稍稍思考一下我们就会发现,多任务这个东西还真是奇妙,它究竟是

      56、怎样做到让多个程序同时运行的呢?如果我们的电脑里面装了好多个CPU的话,同时运行多个程序倒也顺理成章,但实际上就算我们只有一个CPU,照样可以实现多任务。 其实说穿了,这些程序根本没有在同时运行,只不过看上去好像是在同时运行一样:程序A运行一会儿,接下来程序B运行一会儿,再接下来轮到程序C,然后再回到程序A如此反复,有点像日本忍者的“分身术”呢(笑) 。 为了让这种分身术看上去更完美,需要让操作系统尽可能快地切换任务。如果10秒才切换一次,那就连人眼都能察觉出来了,同时运行多个程序的戏码也就穿帮了。再有,如果我们给程序C发出一个按键指令,正巧这个瞬间系统切换到了程序A的话,我们就不得不等上20秒,才能重新轮到程序C对按键指令作出反应。这实在是让人抓狂啊(哭) 。 在一般的操作系统中,这个切换的动作每0.010.03秒就会进行一次。当然,切换的速度越快,让人觉得程序是在同时运行的效果也就越好。不过,CPU进行程序切换(我们称为“任务切换” )这个动作本身就需要消耗一定的时间,这个时间大约为0.0001秒左右,不同的CPU及操作系统所需的时间也有所不同。如果CPU每0.0002秒切换一次任

      57、务的话,该CPU处理能力的50%都要被任务切换本身所消耗掉。这意味着,如果同时运行2个程序,每个程序的速度就只有单独运行时的1/4,这样你会觉得开心吗?如果变成这种结果,那还不如干脆别搞多任务呢。 相比之下,即便是每0.001秒切换一次任务,单单在任务切换上面也要消耗CPU处理能力的10%。大概有人会想,10%也没什么大不了的吧?可如果你看看速度快10%的CPU卖多少钱,说不定就会恍然大悟, “对啊,只要优化一下任务切换间隔,就相当于一分钱也不花,便换上了比现在更快的CPU嘛” (笑) ,你也就明白了浪费10%也是很不值得的。正是因为这个原因,任务切换的间隔最短也得0.01秒左右, 这样一来只有1%的处理能力消耗在任务切换上, 基本上就可以忽略不计了。 284 第 15 天:多任务(1) 关于多任务是什么的问题,已经大致讲得差不多了,接下来我们来看看如何让CPU来处理多任务。 当你向CPU发出任务切换的指令时,CPU会先把寄存器中的值全部写入内存中,这样做是为了当以后切换回这个程序的时候,可以从中断的地方继续运行。接下来,为了运行下一个程序,CPU会把所有寄存器中的值从内存中读取出来(

      58、当然,这个读取的地址和刚刚写入的地址一定是不同的,不然就相当于什么都没变嘛) ,这样就完成了一次切换。我们前面所说的任务切换所需要的时间,正是对内存进行写入和读取操作所消耗的时间。 接下来我们来看看寄存器中的内容是怎样写入内存里去的。 下面这个结构叫做 “任务状态段”(task status segment) ,简称TSS。TSS有16位和32位两个版本,这里我们使用32位版。顾名思义,TSS也是内存段的一种,需要在GDT中进行定义后使用。 struct TSS32 int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3; int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi; int es, cs, ss, ds, fs, gs; int ldtr, iomap; ; 参考上面的结构定义,TSS共包含26个int成员,总计104字节(摘自CPU的技术资料) ,我特意把它们分成4行来写。从开头的backlink起,到cr3为止的几个成员,保存的不是寄存器的数据,而是与任务设置相关的信

      59、息,在执行任务切换的时候这些成员不会被写入(backlink除外,某些情况下是会被写入的) 。后面的部分中我们会用到这里的设定,不过现在你完全可以先忽略它。 第2行的成员是32位寄存器,第3行是16位寄存器,应该没必要解释了吧不对,eip好像到现在还没讲过呢。EIP的全称是“extended instruction pointer” ,也就是“扩展指令指针寄存器”的意思。这里的“扩展”代表它是一个32位寄存器,也就是说其对应的16位版本叫做IP,类比一下的话,跟EAX与AX之间的关系是一样的。 EIP是CPU用来记录下一条需要执行的指令位于内存中哪个地址的寄存器,因此它才被称为“指令指针” 。如果没有这个寄存器,记性不好的CPU就会忘记自己正在运行哪里的程序,于是程序就没办法正常运行了。每执行一条指令,EIP寄存器中的值就会自动累加,从而保证一直指向下一条指令所在的内存地址。 说点题外话,JMP指令实际上是一个向 EIP寄存器赋值的指令。JMP 0x1234这种写法,CPU会解释为 MOV EIP,0x1234,并向 EIP赋值。也就是说,这条指令其实是篡改了 CPU记忆中下一条该执行

      60、的指令的地址,蒙了 CPU 一把。这样一来,CPU 在读取下一条指令时,1 挑战任务切换(harib12a) 285 1 2 3 4 5 8 10 15 9 6 7 12 11 13 14 14 就会去读取 0x1234这个地址中的指令。你看,这不就相当于是做了一个跳转吗? 对了,如果你在汇编语言里用MOV EIP,0x1234这种写法是会出错的,还是不要尝试的好。在汇编语言中,应该使用JMP 0x1234来代替MOV EIP,0x1234。 如果在TSS中将EIP寄存器的值记录下来, 那么当下次再返回这个任务的时候, CPU就可以明白应该从哪里读取程序来运行了。 按照常识,段寄存器应该是16位的才对,可是在TSS数据结构中却定义成了int(也就是DWORD) 类型。 我们可以大胆想象一下, 说不定英特尔公司的人将来会把段寄存器变成32位的,这样想想也挺有意思的呢(笑) 。 第4行的ldtr和iomap也和第1行的成员一样,是有关任务设置的部分,因此在任务切换时不会被CPU写入。也许你会想,那就和第1行一样,暂时先忽略好了但那可是绝对不行的!如果胡乱赋值的话,任务就无法正常切换了,在这

      61、里我们先将ldtr置为0,将iomap置为0x40000000就好了。 关于TSS的话题暂且先告一段落,我们回来继续讲任务切换的方法。要进行任务切换,其实还得用JMP指令。JMP指令分为两种,只改写EIP的称为near模式,同时改写EIP和CS的称为far模式,在此之前我们使用的JMP指令基本上都是near模式的。不记得CS是什么了?CS就是代码段(code segment)寄存器啦。 说起来我们其实用过一次far模式的JMP指令,就在asmhead.nas的“bootpack启动”的最后一句(见8.5节) 。 JMP DWORD 2*8:0x0000001b 这条指令在向EIP存入0x1b的同时, 将CS置为2*8 (=16) 。 像这样在JMP目标地址中带冒号 (:)的,就是far模式的JMP指令。 如果一条JMP指令所指定的目标地址段不是可执行的代码,而是TSS的话,CPU就不会执行通常的改写EIP和CS的操作, 而是将这条指令理解为任务切换。 也就是说, CPU会切换到目标TSS所指定的任务,说白了,就是JMP到一个任务那里去了。 CPU每次执行带有段地址的指令时,都会去确认一

      62、下 GDT中的设置,以便判断接下来要执行的 JMP指令到底是普通的 far-JMP,还是任务切换。也就是说,从汇编程序翻译出来的机器语言来看,普通的 far-JMP和任务切换的 far-JMP,指令本身是没有任何区别的。 286 第 15 天:多任务(1) 好了,枯燥的讲解就到这里,让我们实际做一次任务切换吧。我们准备两个任务:任务A和任务B,尝试从A切换到B。 首先,我们需要创建两个TSS:任务A的TSS和任务B的TSS。 本次的HariMain节选 struct TSS32 tss_a, tss_b; 向它们的ldtr和iomap分别存入合适的值。 本次的HariMain节选 tss_a.ldtr = 0; tss_a.iomap = 0x40000000; tss_b.ldtr = 0; tss_b.iomap = 0x40000000; 接着将它们两个在GDT中进行定义。 本次的HariMain节选 struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT; set_segmdesc(gdt +

      63、 3, 103, (int) &tss_a, AR_TSS32); set_segmdesc(gdt + 4, 103, (int) &tss_b, AR_TSS32); 将tss_a定义在gdt的3号,段长限制为103字节,tss_b也采用类似的定义。 现在两个TSS都创建好了,该进行实际的切换了。 我们向TR寄存器存入3 * 8这个值,这是因为我们刚才把当前运行的任务定义为GDT的3号。TR寄存器以前没有提到过,它的作用是让CPU记住当前正在运行哪一个任务。当进行任务切换的时候,TR寄存器的值也会自动变化,它的名字也就是“task register” (任务寄存器)的缩写。我们每次给TR寄存器赋值的时候,必须把GDT的编号乘以8,因为英特尔公司就是这样规定的。如果你有意见的话,可以打电话找英特尔的大叔投诉哦(笑) 。 给TR寄存器赋值需要使用LTR指令,不过用C语言做不到。唉,各位是不是都已经见怪不怪了啊?啥?你早就料到了? (笑) 所以说, 正如你所料, 我们只能把它写进naskfunc.nas里面。 本次的HariMain节选 load_tr(3 * 8); 1 挑战任务切换(

      64、harib12a) 287 1 2 3 4 5 8 10 15 9 6 7 12 11 13 14 14 本次的naskfunc.nas节选 _load_tr: ; void load_tr(int tr); LTR ESP+4 ; tr RET 对了,LTR指令的作用只是改变TR寄存器的值,因此执行了LTR指令并不会发生任务切换。 要进行任务切换,我们必须执行far模式的跳转指令,可惜far跳转这事C语言还是无能为力,这种语言还真是不方便啊。没办法,这个函数我们也得在naskfunc.nas里创建。 本次的naskfunc.nas节选 _taskswitch4: ; void taskswitch4(void); JMP 4*8:0 RET 也许有人会问,在JMP指令后面写个RET有意义吗?也对,通常情况下确实没意义,因为已经跳转到别的地方了嘛,后面再写什么指令也不会被执行了。不过,用作任务切换的JMP指令却不太一样,在切换任务之后,再返回这个任务的时候,程序会从这条JMP指令之后恢复运行,也就是执行JMP后面的RET,从汇编语言函数返回,继续运行C语言主程序。 另外,如果far-J

      65、MP指令是用作任务切换的话,地址段(冒号前面的4*8的部分)要指向TSS这一点比较重要,而偏移量(冒号后面的0的部分)并没有什么实际作用,会被忽略掉,一般来说像这样写0就可以了。 现在我们需要在HariMain的某个地方来调用taskswitch(),可到底该写在哪里呢?唔,有了,就放在显示“10sec”的语句后面好了。也就是说,程序启动10秒以后进行任务切换。 本次的HariMain节选 else if (i = 10) /* 10秒计时器*/ putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, 10sec, 7); taskswitch4(); /*这里! */ else if (i = 3) /* 3秒计时器 */ 大功告成了?不对,我们还没准备好tss_b呢。在任务切换的时候需要读取tss_b的内容,因此我们得在TSS中定义好寄存器的初始值才行。 本次的HariMain节选 tss_b.eip = (int) &task_b_main; tss_b.eflags = 0x00000202; /* IF = 1

      66、; */ tss_b.eax = 0; 288 第 15 天:多任务(1) tss_b.ecx = 0; tss_b.edx = 0; tss_b.ebx = 0; tss_b.esp = task_b_esp; tss_b.ebp = 0; tss_b.esi = 0; tss_b.edi = 0; tss_b.es = 1 * 8; tss_b.cs = 2 * 8; tss_b.ss = 1 * 8; tss_b.ds = 1 * 8; tss_b.fs = 1 * 8; tss_b.gs = 1 * 8; 乍看之下,貌似会有很多看不懂的地方吧,我们从后半段对寄存器赋值的地方开始看。这里我们给cs置为GDT的2号,其他寄存器都置为GDT的1号,asmhead.nas的时候也是一样的。也就是说,我们这次使用了和bootpack.c相同的地址段。当然,如果你用别的地址段也没问题,不过这次我们只是想随便做个任务切换的实验而已,这种麻烦的事情还是以后再说吧。 继续看剩下的部分,关于eflags的赋值,如果把STI后的EFLAGS的值通过io_load_eflags赋给变量的话,该变量的值

      67、就显示为0x00000202,因此在这里就直接使用了这个值,仅此而已。如果还有看不懂的地方,大概就是eip和esp的部分了吧。 在eip中,我们需要定义在切换到这个任务的时候,要从哪里开始运行。在这里我们先把task_b_main这个函数的内存地址赋值给它。 本次的bootpack.c节选 void task_b_main(void) for (;) io_hlt(); 这个函数只执行了一个HLT,没有任何实际作用,后面我们会对它进行各种改造,现在就先这样吧。 task_b_esp是专门为任务B所定义的栈。有人可能会说,直接用任务A的栈不就好了吗?那可不行,如果真这么做的话,栈就会混成一团,程序也无法正常运行。 2 任务切换进阶(harib12b) 289 1 2 3 4 5 8 10 15 9 6 7 12 11 13 14 14 本次的HariMain节选 int task_b_esp; task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024; 总之先写成这个样子了。我们为任务B的栈分配了64KB的内存,并计算出栈底的

      68、内存地址。请各位回忆一下向栈PUSH数据(入栈)的动作,ESP中存入的应该栈末尾的地址,而不是栈开头的地址。 好了,我们已经讲解得够多了,现在总算是万事俱备啦,马上“make run”一下吧。这个程序如果运行正常的话应该是什么样子呢?嗯,启动之后的10秒内,还是跟以前一样的,10秒一到便执行任务切换,task_b_main开始运行。因为task_b_main只有一句HLT,所以接下来程序就全部停止了,鼠标和键盘也应该都没有反应了。 唔这样看起来好像很无聊啊,算了,总之我们先来“make run”吧。10秒钟的等待还真是漫长哇!停了停了! 看来我们的首次任务切换获得了圆满成功。 输入到一半就停住了哦! 2 任务切换进阶(harib12b) 刚才我们只是实现了一次性从任务A切换到任务B,现在我们要尝试再切换回任务A。好,那我们就在切换到任务B的5秒后,让它再切换回任务A吧。 这其实很容易,只要稍微改写一下task_b_main就可以了。 2 290 第 15 天:多任务(1) 本次的bootpack.c节选 void task_b_main(void) struct FIFO32 fifo

      69、; struct TIMER *timer; int i, fifobuf128; fifo32_init(&fifo, 128, fifobuf); timer = timer_alloc(); timer_init(timer, &fifo, 1); timer_settime(timer, 500); for (;) io_cli(); if (fifo32_status(&fifo) = 0) io_stihlt(); else i = fifo32_get(&fifo); io_sti(); if (i = 1) /*超时时间为5秒 */ taskswitch3(); /*返回任务A */ 你看,这样就搞定了。在这里所使用的变量名,比如fifo、timer等,和HariMain里面是一样的,不过别担心,计算机会把它们当成不同的变量来处理。无论我们对这里的变量如何赋值,都不会影响到HariMain中的对应变量。这并不是因为它们处于不同的任务,而是因为它们名字虽然一样,但实际上根本是不同的变量(之前一直没有机会解释这一点,现在稍微晚了点,不过还是在这里讲一下吧) 。 对了,tas

      70、kswitch3还没有创建,我们需要创建它。 本次的naskfunc.nas节选 _taskswitch3: ; void taskswitch3(void); JMP 3*8:0 RET 好了,准备完毕! 我们来“make run”一下。哇,经过10秒之后光标停止闪烁,鼠标没有反应,键盘也无法输入文字了。然而又过了5秒,光标又重新开始闪烁,刚才键盘没反应的时候打进去的字一口气全都冒了出来,鼠标也又能动了。 这就说明我们已经成功回到了任务A并继续运行了,真顺利呀。 3 做个简单的多任务(1) (harib12c) 291 1 2 3 4 5 8 10 15 9 6 7 12 11 13 14 14 3 做个简单的多任务(1)(harib12c) 接下来,我们要实现更快速的,而且是来回交替的任务切换。这样一来,我们就可以告别光标停住、鼠标卡死、键盘打不了字等情况,从而让两个任务看上去好像在同时运行一样。 在开始动手之前,笔者认为像taskswitch3、taskswitch4这种写法实在不好。假设我们有100个任务,难道就要创建100个任务切换函数不成?这样肯定不行,最好是写成一个函数,

      71、比如像taskswitch(3);这样。 为了解决这个问题,我们先创建这样一个函数。 本次的naskfunc.nas节选 _farjmp: ; void farjmp(int eip, int cs); JMP FAR ESP+4 ; eip, cs RET “JMP FAR”指令的功能是执行far跳转。在JMP FAR指令中,可以指定一个内存地址,CPU会从指定的内存地址中读取4个字节的数据,并将其存入EIP寄存器,再继续读取2个字节的数据,并将其存入CS寄存器。当我们调用这个函数,比如farjmp(eip,cs);,在ESP+4这个位置就存放了eip的值,而ESP+8则存放了cs的值,这样就可以实现far跳转了。 因此我们需要将调用的部分改写如下: taskswitch3(); farjmp(0, 3 * 8); taskswitch4(); farjmp(0, 4 * 8); 现在我们来缩短切换的间隔。 在任务A和任务B中, 分别准备一个timer_ts变量, 以便每隔0.02秒执行一次任务切换。这个变量名中的ts就是“task switch”的缩写,代表“任务切换计时器”的意思

      72、。 本次的bootpack.c节选 void HariMain(void) (中略) timer_ts = timer_alloc(); timer_init(timer_ts, &fifo, 2); timer_settime(timer_ts, 2); (中略) for (;) io_cli(); 3 292 第 15 天:多任务(1) if (fifo32_status(&fifo) = 0) io_stihlt(); else i = fifo32_get(&fifo); io_sti(); if (i = 2) farjmp(0, 4 * 8); timer_settime(timer_ts, 2); else if (256 = i & i = 511) /*键盘数据*/ (中略) else if (512 = i & i = 767) /*鼠标数据*/ (中略) else if (i = 10) /* 10秒计时器*/ putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, 10sec, 7); else

      73、if (i = 3) /* 3秒计时器*/ putfonts8_asc_sht(sht_back, 0, 80, COL8_FFFFFF, COL8_008484, 3sec, 6); else if (i timeout timerctl.count) break; /*超时*/ timer-flags = TIMER_FLAGS_ALLOC; if (timer != mt_timer) fifo32_put(timer-fifo, timer-data); else ts = 1; /* mt_timer超时*/ timer = timer-next; /*将下一个计时器的地址赋给timer */ timerctl.t0 = timer; timerctl.next = timer-timeout; if (ts != 0) mt_taskswitch(); return; 在这里,如果产生超时的计时器是mt_timer的话,不向FIFO写入数据,而是将ts置为1。最后7 多任务进阶(harib12g) 301 1 2 3 4 5 8 10 15 9 6 7 12 11 13 1

      74、4 14 判断如果ts的值不为0,就调用mt_taskswitch进行任务切换。 看了上面这段代码,你可能会问,为什么要用ts这个变量呢?在 /* 超时 */ 的地方直接调用mt_taskswitch不就好了吗?也就是下面这样: 出问题的例子 void inthandler20(int *esp) (中略) for (;) /* timers的计时器全部在工作中,因此不用确认flags */ if (timer-timeout timerctl.count) break; /*超时*/ timer-flags = TIMER_FLAGS_ALLOC; if (timer != mt_timer) fifo32_put(timer-fifo, timer-data); else mt_taskswitch(); timer = timer-next; /*将下一个计时器的地址赋给timer */ timerctl.t0 = timer; timerctl.next = timer-timeout; return; 为什么不这样写呢?这样写的确可以让代码更简短,但是会出问题。 出问题的原因

      75、在于, 调用mt_taskswitch进行任务切换的时候, 即便中断处理还没完成, IF (中断允许标志) 的值也可能会被重设回1 (因为任务切换的时候会同时切换EFLAGS) 。 这样可不行,在中断处理还没完成的时候,可能会产生下一个中断请求,这会导致程序出错。 因此我们需要采用这样的设计等中断处理全部完成之后,再在必要时调用mt_taskswitch。 接下来我们只需要将HariMain和task_b_main里面有关任务切换的代码删掉即可。 删代码没什么难度,而且HariMain又很长,为了节约纸张我们就省略了,只把task_b_main写在下面吧。 本次的bootpack.c节选 void task_b_main(struct SHEET *sht_back) struct FIFO32 fifo; struct TIMER *timer_put, *timer_1s; 302 第 15 天:多任务(1) int i, fifobuf128, count = 0, count0 = 0; char s12; fifo32_init(&fifo, 128, fifobuf); t

      76、imer_put = timer_alloc(); timer_init(timer_put, &fifo, 1); timer_settime(timer_put, 1); timer_1s = timer_alloc(); timer_init(timer_1s, &fifo, 100); timer_settime(timer_1s, 100); for (;) count+; io_cli(); if (fifo32_status(&fifo) = 0) io_sti(); else i = fifo32_get(&fifo); io_sti(); if (i = 1) sprintf(s, %11d, count); putfonts8_asc_sht(sht_back, 0, 144, COL8_FFFFFF, COL8_008484, s, 11); timer_settime(timer_put, 1); else if (i = 100) sprintf(s, %11d, count - count0); putfonts8_asc_sht(sht_back, 0,

      77、128, COL8_FFFFFF, COL8_008484, s, 11); count0 = count; timer_settime(timer_1s, 100); 像这样,把有关任务切换的部分全部删掉就可以了。 好,我们来试试看能不能正常工作吧。 “make run” ,成功了,真开心!不过看上去和之前没什么区别。 和上一节相比, 为什么现在的设计可以称为 “真正的多任务” 呢?因为如果使用这样的设计,即便在程序中不进行任务切换的处理(比如忘记写了,或者因为bug没能正常切换之类的) ,也一定会正常完成切换。之前那种多任务的话,如果任务B因为发生bug而无法进行切换,那么当切换到任务B以后,其他的任务就再也无法运行了,这样会造成无论是按键盘还是动鼠标都毫无反应的悲剧。 7 多任务进阶(harib12g) 303 1 2 3 4 5 8 10 15 9 6 7 12 11 13 14 14 真正的多任务也成功了! 真正的多任务不会发生这样的问题,因此这种方式更好话虽如此,但其实即便是harib12g,在任务B发生bug的情况下,也有可能出现键盘输入失去响应的问题。例如,明明写了io

      78、_cli();却忘记写io_sti();的话,中断就会一直处于禁止状态,即使产生了计时器中断请求,也不会被传递给中断处理程序。这样一来,mt_taskswitch当然也就不会被调用,这意味着任务切换也就不会被执行。 其实CPU已经为大家准备了解决这个问题的方法,因此我们稍后再考虑这个问题吧。 好,我们在真机环境下运行一下,看看速度会不会变慢。咦?速度非但没有变慢,反而变快了?运行结果是6493300,和之前的14281323相比,性能的差距是2.2倍。harib12f的时候还是差3倍来着,这次也太快了吧。我们再把timer_settime(timer_put,1);删掉,看看如果不显示计数只显示速度会怎样?说不定速度会变得更快呢?哇!结果出来了,6890930,居然达到了2.07倍,离理想值2.0倍又近了一步呢。 现在想想看,为什么速度反而会变快呢?我想这是因为在任务切换的时候,我们不再使用FIFO缓冲区的缘故。之前我们向FIFO中写入超时的编号,然后从中读取这个编号来判断是否执行任务切换,相比之下,现在的做法貌似对于CPU来说负担更小些,一定是这个原因吧。 哎呀,不知不觉就已经很晚了

      79、。今天就先到这里吧,我们明天继续。 多任务(2) 任务管理自动化(harib13a) 让任务休眠(harib13b) 增加窗口数量(harib13c) 设定任务优先级(1) (harib13d) 设定任务优先级(2) (harib13e) 1 任务管理自动化(harib13a) 大家好!昨天我们已经实践了很多关于多任务的内容,不过今天我们还得继续讲多任务。可能有人会说, “老是讲多任务都听腻了啊! ” ,但多任务真的非常重要(当然,如果你不想做一个多任务的操作系统,那就不重要啦) 。从笔者的角度来说,希望大家能够在充分做好多任务机制的基础上,再利用多任务逐步完善操作系统本身。因此,大家再稍微忍耐一下吧。 在15.7节中,我们已经实现了真正的多任务,不过这样还不够完善,或者说不太好用。如果我们想要运行三个任务的话,就必须改写mt_taskswitch的代码。笔者认为,这样的设计实在太逊了,如果能像当初定时器和窗口背景的做法一样(具体如下) ,是不是觉得更好呢? task = task_alloc(); task-tss.eip = ; task-tss.esp = ; 像上面这样设定各种

      80、寄存器的初始值像上面这样设定各种寄存器的初始值 task_run(task); 我们就先以此为目标,对代码进行改造吧。 于是我们写了下面这样一段程序,struct TASKCTL是仿照struct SHTCTL写出来的,首先我们来看结构定义。 第 16 天 1 1 任务管理自动化(harib13a) 305 16 2 3 4 5 8 10 16 9 6 7 12 11 13 15 14 本次的bootpack.h节选 #define MAX_TASKS 1000 /*最大任务数量*/ #define TASK_GDT0 3 /*定义从GDT的几号开始分配给TSS */ struct TSS32 int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3; int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi; int es, cs, ss, ds, fs, gs; int ldtr, iomap; ; struct TASK int sel, flags; /* sel用来存放GDT的编

      81、号*/ struct TSS32 tss; ; struct TASKCTL int running; /*正在运行的任务数量*/ int now; /*这个变量用来记录当前正在运行的是哪个任务*/ struct TASK *tasksMAX_TASKS; struct TASK tasks0MAX_TASKS; ; 下面我们来创建用来对struct TASKCTL及其当中包含的struct TASK进行初始化的函数task_init。由于struct TASKCTL是一个很庞大的结构,因此我们让它从memman_alloc来申请内存空间。这个函数是用来替代mt_init使用的。 我们使用sel这个变量来存放GDT编号,sel是“selector”的缩写,意为选择符。因为英特尔的大叔管段地址叫做selector,所以笔者只是照猫画虎而已,也就是代表“应该从GDT里面选择哪个编号”的意思。 本次的mtask.c节选 struct TASKCTL *taskctl; struct TIMER *task_timer; struct TASK *task_init(struct MEMMAN

      82、 *memman) int i; struct TASK *task; struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT; taskctl = (struct TASKCTL *) memman_alloc_4k(memman, sizeof (struct TASKCTL); for (i = 0; i tasks0i.flags = 0; taskctl-tasks0i.sel = (TASK_GDT0 + i) * 8; set_segmdesc(gdt + TASK_GDT0 + i, 103, (int) &taskctl-tasks0i.tss, AR_TSS32); task = task_alloc(); 306 第 16 天:多任务(2) task-flags = 2; /*活动中标志*/ taskctl-running = 1; taskctl-now = 0; taskctl-tasks0 = task; load_tr(task-sel); task_timer = ti

      83、mer_alloc(); timer_settime(task_timer, 2); return task; 调用task_init,会返回一个内存地址,意思是“现在正在运行的这个程序,已经变成一个任务了” 。可能大家不是很能理解这个说法,在调用init之后,所有程序的运行都会被当成任务来进行管理,而调用init的这个程序,我们也要让它所属于某个任务,这样一来,通过调用任务的设置函数,就可以对任务进行各种控制,比如说修改优先级等。 下面我们来创建用来初始化一个任务结构的函数。 本次的mtask.c节选 struct TASK *task_alloc(void) int i; struct TASK *task; for (i = 0; i tasks0i.flags = 0) task = &taskctl-tasks0i; task-flags = 1; /*正在使用的标志*/ task-tss.eflags = 0x00000202; /* IF = 1; */ task-tss.eax = 0; /*这里先置为0*/ task-tss.ecx = 0; task-tss.edx

      84、 = 0; task-tss.ebx = 0; task-tss.ebp = 0; task-tss.esi = 0; task-tss.edi = 0; task-tss.es = 0; task-tss.ds = 0; task-tss.fs = 0; task-tss.gs = 0; task-tss.ldtr = 0; task-tss.iomap = 0x40000000; return task; return 0; /*全部正在使用*/ 1 任务管理自动化(harib13a) 307 16 2 3 4 5 8 10 16 9 6 7 12 11 13 15 14 关于寄存器的初始值,这里先随便设置了一下。如果不喜欢这个值,可以在bootpack.c里面设置一下。 接下来是task_run,这个函数非常短,看看这样写如何。 本次的mtask.c节选 void task_run(struct TASK *task) task-flags = 2; /*活动中标志*/ taskctl-taskstaskctl-running = task; taskctl-running+;

      85、return; 这个函数的作用是,将task添加到tasks的末尾,然后使running加1,仅此而已。 最后是task_switch,这个函数用来代替mt_taskswitch。 在timer.c中对mt_taskswitch的调用,也相应地修改为调用task_switch。 本次的mtask.c节选 void task_switch(void) timer_settime(task_timer, 2); if (taskctl-running = 2) taskctl-now+; if (taskctl-now = taskctl-running) taskctl-now = 0; farjmp(0, taskctl-taskstaskctl-now-sel); return; 当running为1时,不需要进行任务切换,函数直接结束。当running大于等于2时,先把now加1,然后把now所代表的任务切换成当前任务,最后再将末尾的任务移动到开头。 现在我们用以上这些结构和函数,将bootpack.c改写一下。 308 第 16 天:多任务(2) 本次的bootpack.c节选

      86、void HariMain(void) (中略) struct TASK *task_b; (中略) task_init(memman); task_b = task_alloc(); task_b-tss.esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8; task_b-tss.eip = (int) &task_b_main; task_b-tss.es = 1 * 8; task_b-tss.cs = 2 * 8; task_b-tss.ss = 1 * 8; task_b-tss.ds = 1 * 8; task_b-tss.fs = 1 * 8; task_b-tss.gs = 1 * 8; *(int *) (task_b-tss.esp + 4) = (int) sht_back; task_run(task_b); (中略) 行数变少了,不过相应地mtask.c却变长了,好像也不能说是非常好。不过好在,在HariMain中,就再也不用管GDT到底怎样、任务B的tss要分配到GDT的几号等。这些麻烦的事情,全

      87、部交给mtask.c来处理了。 当需要增加任务数量的时候,不用再像之前那样修改task_switch了,只要先task_alloc,然后再task_run就行了。 好了,我们来运行一下, “make run” 。不错,貌似成功了。 2 让任务休眠(harib13b) 直到harib13a为止,我们所实现的多任务,是为两个任务分配大约相同的运行时间。这也不能说是不好,不过相比之下,任务A明显空闲的时间比较多。没有键盘输入、没有鼠标操作也不会经常出现定时器中断这样的情况,这个时候任务A没什么事做,就只好HLT了。 HLT的时候能省电,也不错嘛!不过当任务B全力以赴拼命干活的时候,任务A却在无所事事,这样好像不太好。笔者觉得,与其让任务A闲着没事干,还不如把这些时间分给繁忙的任务B呢。 那么怎样才能避免任务A浪费时间呢?如果我们不让任务A去HLT,而是把它从taskctl tasks中删掉的话,嗯,这应该是一个不错的主意。如果把任务A从tasks中删掉,只保留任务B,那任务B就可以全力以赴工作了。像这样将一个任务从tasks中删除的操作,用多任务中的术语来说叫做“休眠” (sleep) 。

      88、2 2 让任务休眠(harib13b) 309 16 2 3 4 5 8 10 16 9 6 7 12 11 13 15 14 不过这样也有一个问题,当任务A休眠时,即便FIFO有数据过来,也无法响应了,这可不行,当FIFO有数据过来的时候,必须要把任务A唤醒。怎么唤醒呢?其实只要再运行一次task_run就可以了(笑) 。 首先我们创建task_sleep。 本次的mtask.c节选 void task_sleep(struct TASK *task) int i; char ts = 0; if (task-flags = 2) /*如果指定任务处于唤醒状态*/ if (task = taskctl-taskstaskctl-now) ts = 1; /*让自己休眠的话,稍后需要进行任务切换*/ /*寻找task所在的位置*/ for (i = 0; i running; i+) if (taskctl-tasksi = task) /*在这里*/ break; taskctl-running-; if (i now) taskctl-now-; /*需要移动成员,要相应地处理*/

      89、 /*移动成员*/ for (; i running; i+) taskctl-tasksi = taskctl-tasksi + 1; task-flags = 1; /*不工作的状态*/ if (ts != 0) /*任务切换*/ if (taskctl-now = taskctl-running) /*如果now的值出现异常,则进行修正*/ taskctl-now = 0; farjmp(0, taskctl-taskstaskctl-now-sel); return; 这次的程序比较长,所以多加了一些注释。类似任务A强行让任务B休眠这样“一个任务让另一个任务休眠”的情形还是很简单的,只要在tasks中搜索该任务,找到后用后面的成员填充过来即可。 问题是类似任务A让任务A休眠这样“自己让自己休眠”的情形,因为是要让当前正在运行310 第 16 天:多任务(2) 的任务休眠,因此在处理结束之后必须马上切换到下一个任务。只要注意以上两点,task_sleep的代码还是不难理解的。 接下来是当FIFO中写入数据的时候将任务唤醒的功能。首先,我们要在FIFO的结构定义中,添加用于记录要唤

      90、醒任务的信息的成员。 本次的bootpack.h节选 struct FIFO32 int *buf; int p, q, size, free, flags; struct TASK *task; ; 然后我们改写fifo32_init,让它可以在参数中指定一个任务。如果不想使用任务自动唤醒功能的话,只要将task置为0即可。 本次的fifo.c节选 void fifo32_init(struct FIFO32 *fifo, int size, int *buf, struct TASK *task) /* FIFO缓冲区初始化*/ fifo-size = size; fifo-buf = buf; fifo-free = size; /*剩余空间*/ fifo-flags = 0; fifo-p = 0; /*写入位置*/ fifo-q = 0; /*读取位置*/ fifo-task = task; /*有数据写入时需要唤醒的任务*/ /*这里! */ return; 接着,我们来实现当向FIFO写入数据时,唤醒某个任务的功能。 int fifo32_put(struct FIFO3

      91、2 *fifo, int data) /*向FIFO写入数据并累积起来*/ if (fifo-free = 0) /*没有剩余空间则溢出*/ fifo-flags |= FLAGS_OVERRUN; return -1; fifo-buffifo-p = data; fifo-p+; if (fifo-p = fifo-size) fifo-p = 0; 2 让任务休眠(harib13b) 311 16 2 3 4 5 8 10 16 9 6 7 12 11 13 15 14 fifo-free-; if (fifo-task != 0) /*从这里开始*/ if (fifo-task-flags != 2) /*如果任务处于休眠状态*/ task_run(fifo-task); /*将任务唤醒*/ /*到这里结束*/ return 0; 我们追加了5行代码。 在这里如果任务已经处于唤醒状态的话, 再次对其task_run是不行的 (会造成任务重复注册) ,因此我们需要先确认该任务是否处于休眠状态,然后再将其唤醒。 最后我们来改写HariMain。 本次的bootpack.c节选 vo

      92、id HariMain(void) struct TASK *task_a, *task_b; (中略) fifo32_init(&fifo, 128, fifobuf, 0); (中略) task_a = task_init(memman); fifo.task = task_a; (中略) for (;) io_cli(); if (fifo32_status(&fifo) = 0) task_sleep(task_a); io_sti(); else (中略) void task_b_main(struct SHEET *sht_back) (中略) fifo32_init(&fifo, 128, fifobuf, 0); (中略) 最开始的fifo32_init中指定任务的参数,我们用0代替了。因为我们在任务A中应用了休眠,也就需要使用让FIFO来唤醒的功能, 不过在这个时间点上多任务的初始化还没有完成, 因此无法312 第 16 天:多任务(2) 指定任务,只能先在这里用0代替,也就是禁用自动唤醒功能。 随后,在task_init中会返回自己的构造地址,我们将这个地址存入fi

      93、fo.task。 这样一来,当FIFO为空的时候,任务A将执行task_sleep来替代之前的HLT。关于io_sti和task_sleep的顺序,需要稍微动点脑筋。如果先STI的话,在进行休眠处理的时候可能会发生中断请求,FIFO里面就会写入数据,这样有可能会发生无法成功唤醒等异常情况。因此,我们需要在禁止中断请求的状态下进行休眠处理,然后在唤醒之后马上执行STI。 task_b_main不需要让FIFO唤醒,因此任务参数指定为0。 那么,这样做是否能成功呢,我们来试试看。 “make run” ,哇,速度很快。请注意看速度显示的数字。 速度变快了? 在真机环境下进行测试,在不操作鼠标和键盘的情况下,速度可以达到13438300,harib12g的成绩是6493300,相比之下速度达到了2倍以上。 大家可能会觉得速度超过2倍这一点有点无法理解,其实这是正常的,因为两者都是以每0.01秒刷新一次count的显示。 假定harib12g的任务B的处理能力为每秒100。 其中的10用于显示count,剩下的90用于计算count+;。 假定harib13b的任务B的处理能力为每秒198。

      94、(之所以无法达到200,是因为休眠的任务A并不是完全不需要消耗处理能力) 用于显示count的还是其中的10,剩下的188用于计算count+;。 188是90的2倍以上。 基本上就是这样的原理。 3 增加窗口数量(harib13c) 313 16 2 3 4 5 8 10 16 9 6 7 12 11 13 15 14 如果用鼠标不停移动窗口,再加上用键盘不断打字的话,由于任务A会变得比较忙碌,任务B的速度就会相应下降,大约会降到8700000左右。如果我们能让任务A更忙碌一些的话,最终任务B的速度会下降到6493300,也就是任务A完全不休眠时的值。 和harib10i时的成绩14281323相比,这次的13438300还有6%左右的差距,这应该是每0.01秒刷新一次显示导致的,因此我们把这个去掉之后再来测试一下速度。做法和之前有所不同,采用的是将task_b_main中的两处timer_settime(timer_put, 1);改成timer_settime(timer_put. 100);的方法。测试结果为13782000,差距缩小到3%左右了,可以说和没有多任务的状态非常接

      95、近了。 其实一开始笔者也是按照之前的方法,把timer_settime(timer_put, 1);删掉来测试速度的,但测出来的结果居然令人震惊地高达36343300,这相当于14281323的2.5倍。速度比没有多任务的时候还快,这个结果是十分异常的,这是由于JMP指令的跳转目标地址发生变化所致。 因此,为了不让跳转目标地址发生变化,我们只好保留那条语句。本来想把timer_put的中断间隔设置为1000或者10000左右的,不过这样一来指令的长度会发生变化,所以只好设置成100了。 从这个问题来看,用C语言测试速度还是有局限性的。如果要精确比较速度的话,只能在仔细考虑地址问题的前提下,用汇编语言来编写程序来实现了。 3 增加窗口数量(harib13c) 在16.1节中我们已经对任务的新增做了简化,因此接下来我们要为系统增加更多的任务,即形成任务A、任务B0、任务B1和任务B2的格局。 任务B0B2各自拥有自己的窗口,它们的功能都一样,即进行计数,这有点像在Windows中启动了一个应用程序及其2个副本的感觉。对了,任务A的3秒定时和10秒定时我们已经不需要了,因此将它们删去。 本次

      96、的bootpack.c节选 void HariMain(void) (中略) unsigned char *buf_back, buf_mouse256, *buf_win, *buf_win_b; struct SHEET *sht_back, *sht_mouse, *sht_win, *sht_win_b3; struct TASK *task_a, *task_b3; struct TIMER *timer; (中略) 3 314 第 16 天:多任务(2) init_palette(); shtctl = shtctl_init(memman, binfo-vram, binfo-scrnx, binfo-scrny); task_a = task_init(memman); fifo.task = task_a; /* sht_back */ sht_back = sheet_alloc(shtctl); buf_back = (unsigned char *) memman_alloc_4k(memman, binfo-scrnx * binfo-scrny); shee

      97、t_setbuf(sht_back, buf_back, binfo-scrnx, binfo-scrny, -1); /*无透明色 */ init_screen8(buf_back, binfo-scrnx, binfo-scrny); /* sht_win_b */ for (i = 0; i tss.esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8; task_bi-tss.eip = (int) &task_b_main; task_bi-tss.es = 1 * 8; task_bi-tss.cs = 2 * 8; task_bi-tss.ss = 1 * 8; task_bi-tss.ds = 1 * 8; task_bi-tss.fs = 1 * 8; task_bi-tss.gs = 1 * 8; *(int *) (task_bi-tss.esp + 4) = (int) sht_win_bi; task_run(task_bi); /* sht_win */ sht_win = sheet_alloc(

      98、shtctl); buf_win = (unsigned char *) memman_alloc_4k(memman, 160 * 52); sheet_setbuf(sht_win, buf_win, 144, 52, -1); /*无透明色*/ make_window8(buf_win, 144, 52, task_a, 1); make_textbox8(sht_win, 8, 28, 128, 16, COL8_FFFFFF); cursor_x = 8; cursor_c = COL8_FFFFFF; timer = timer_alloc(); timer_init(timer, &fifo, 1); timer_settime(timer, 50); /* sht_mouse */ sht_mouse = sheet_alloc(shtctl); sheet_setbuf(sht_mouse, buf_mouse, 16, 16, 99); init_mouse_cursor8(buf_mouse, 99); mx = (binfo-scrnx - 16) / 2; /

      99、*计算坐标使其位于画面中央*/ my = (binfo-scrny - 28 - 16) / 2; sheet_slide(sht_back, 0, 0); sheet_slide(sht_win_b0, 168, 56); sheet_slide(sht_win_b1, 8, 116); 3 增加窗口数量(harib13c) 315 16 2 3 4 5 8 10 16 9 6 7 12 11 13 15 14 sheet_slide(sht_win_b2, 168, 116); sheet_slide(sht_win, 8, 56); sheet_slide(sht_mouse, mx, my); sheet_updown(sht_back, 0); sheet_updown(sht_win_b0, 1); sheet_updown(sht_win_b1, 2); sheet_updown(sht_win_b2, 3); sheet_updown(sht_win, 4); sheet_updown(sht_mouse, 5); sprintf(s, (%3d, %3d), mx,

      100、my); putfonts8_asc_sht(sht_back, 0, 0, COL8_FFFFFF, COL8_008484, s, 10); sprintf(s, memory %dMB free : %dKB, memtotal / (1024 * 1024), memman_total(memman) / 1024); putfonts8_asc_sht(sht_back, 0, 32, COL8_FFFFFF, COL8_008484, s, 40); for (;) io_cli(); if (fifo32_status(&fifo) = 0) task_sleep(task_a); io_sti(); else i = fifo32_get(&fifo); io_sti(); if (256 = i & i = 511) /*键盘数据*/ (中略) else if (512 = i & i = 767) /*光标用定时器*/ (中略) else if (i = 1) /*光标用定时器*/ (中略) void make_window8(unsigned char *buf,

      101、 int xsize, int ysize, char *title, char act) (中略) char c, tc, tbc; if (act != 0) tc = COL8_FFFFFF; tbc = COL8_000084; else tc = COL8_C6C6C6; tbc = COL8_848484; (中略) boxfill8(buf, xsize, tbc, 3, 3, xsize - 4, 20 ); boxfill8(buf, xsize, COL8_848484, 1, ysize - 2, xsize - 2, ysize - 2); boxfill8(buf, xsize, COL8_000000, 0, ysize - 1, xsize - 1, ysize - 1); putfonts8_asc(buf, xsize, 24, 4, tc, title); for (y = 0; y flags = 2; /*活动中标志*/ task-priority = 2; /* 0.02秒 */ taskctl-running = 1; taskctl-now

      102、 = 0; taskctl-tasks0 = task; 4 318 第 16 天:多任务(2) load_tr(task-sel); task_timer = timer_alloc(); timer_settime(task_timer, task-priority); return task; 对task_init的改写很简单,没有什么需要特别说明的地方。在这里,我们给最开始的任务设定了0.02秒这个标准值。 接下来是用来运行任务的task_run,我们让它可以通过参数来设定优先级。 本次的mtask.c节选 void task_run(struct TASK *task, int priority) if (priority 0) task-priority = priority; if (task-flags != 2) task-flags = 2; /*活动中标志*/ taskctl-taskstaskctl-running = task; taskctl-running+; return; 上面的代码中,一开始我们先判断了priority的值,当为0时则表示不改变当前已

      103、经设定的优先级。这样的设计主要是为了在唤醒休眠任务的时候使用。 此外,即使该任务正在运行,我们也能使用task_run仅改变任务的优先级。 接着是task_switch,我们要让它在设置定时器的时候,应用priority的值。 本次的mtask.c节选 void task_switch(void) struct TASK *task; taskctl-now+; if (taskctl-now = taskctl-running) taskctl-now = 0; task = taskctl-taskstaskctl-now; timer_settime(task_timer, task-priority); if (taskctl-running = 2) farjmp(0, task-sel); 4 设定任务优先级(1) (harib13d) 319 16 2 3 4 5 8 10 16 9 6 7 12 11 13 15 14 return; 当只有一个任务的时候,如果执行farjmp(0, tasksel);的话,虽然不会真的切换但确实是发出了任务切换的指令。这时CPU会认为

      104、“操作系统怎么会做这种毫无意义的事情呢?这一定是操作系统的bug! ”因而拒绝执行该指令,程序运行就会乱套。所以我们需要在farjmp之前,判断任务数量是否在2个以上。 这样一来,对mtask.c的改写就OK了。 现在我们来改写fifo.c。从休眠状态唤醒任务的时候需要调用task_run,我们这次主要就是改写这个地方。说白了,其实我们只是将任务唤醒,并不改变其优先级,因此只要将优先级设置为0就可以了。 本次的fifo.c节选 int fifo32_put(struct FIFO32 *fifo, int data) /*向FIFO写入数据并累积起来*/ (中略) fifo-free-; if (fifo-task != 0) if (fifo-task-flags != 2) /*如果任务处于休眠状态*/ task_run(fifo-task, 0); /*将任务唤醒*/ return 0; 最后我们来改写一下HariMain,做一做改变优先级的实验。 本次的bootpack.c节选 void HariMain(void) (中略) /* sht_win_b */ for (i =

      105、0; i leveltaskctl-now_lv; return tl-taskstl-now; 5 设定任务优先级(2) (harib13e) 323 16 2 3 4 5 8 10 16 9 6 7 12 11 13 15 14 这里面包含很多结构,比较繁琐,不过仔细看看应该就能明白。 task_add函数,用来向struct TASKLEVEL中添加一个任务。 本次的mtask.c节选 void task_add(struct TASK *task) struct TASKLEVEL *tl = &taskctl-leveltask-level; tl-taskstl-running = task; tl-running+; task-flags = 2; /*活动中*/ return; 实际上,这里应该增加if(tl running leveltask-level; /*寻找task所在的位置*/ for (i = 0; i running; i+) if (tl-tasksi = task) /*在这里 */ break; tl-running-; if (i now) tl

      106、-now-; /*需要移动成员,要相应地处理 */ if (tl-now = tl-running) /*如果now的值出现异常,则进行修正*/ tl-now = 0; task-flags = 1; /* 休眠中 */ /* 移动 */ for (; i running; i+) 324 第 16 天:多任务(2) tl-tasksi = tl-tasksi + 1; return; 上面的代码基本上是照搬了task_sleep的内容。 task_switchsub函数,用来在任务切换时决定接下来切换到哪个LEVEL。 本次的mtask.c节选 void task_switchsub(void) int i; /*寻找最上层的LEVEL */ for (i = 0; i leveli.running 0) break; /*找到了*/ taskctl-now_lv = i; taskctl-lv_change = 0; return; 到目前为止,和struct TASKLEVEL相关的函数已经差不多都写好了,准备工作做到这里,接下来的事情就简单多了。 下面我们来改写其他一些函数,首

      107、先是task_init。最开始的任务,我们先将它放在LEVEL 0,也就是最高优先级LEVEL中。这样做在有些情况下可能会有问题,不过后面可以再用task_run重新设置,因此不用担心。 本次的mtask.c节选 struct TASK *task_init(struct MEMMAN *memman) (中略) for (i = 0; i leveli.running = 0; taskctl-leveli.now = 0; task = task_alloc(); task-flags = 2; /*活动中标志*/ task-priority = 2; /* 0.02秒*/ task-level = 0; /*最高LEVEL */ task_add(task); task_switchsub(); /* LEVEL 设置*/ 5 设定任务优先级(2) (harib13e) 325 16 2 3 4 5 8 10 16 9 6 7 12 11 13 15 14 load_tr(task-sel); task_timer = timer_alloc(); timer_settime(t

      108、ask_timer, task-priority); return task; 开始的时候只有LEVEL 0中有一个任务,因此我们按照这样的方式来进行初始化。 下面是task_run,我们要让它可以在参数中指定LEVEL。 本次的mtask.c节选 void task_run(struct TASK *task, int level, int priority) if (level level; /*不改变LEVEL */ if (priority 0) task-priority = priority; if (task-flags = 2 & task-level != level) /*改变活动中的LEVEL */ task_remove(task); /*这里执行之后flag的值会变为1,于是下面的if语句块也会被执行*/ if (task-flags != 2) /*从休眠状态唤醒的情形*/ task-level = level; task_add(task); taskctl-lv_change = 1; /*下次任务切换时检查LEVEL */ return; 在此之前,t

      109、ask_run中下一个要切换到的任务是固定不变的,不过现在情况就不同了。例如,如果用task_run启动了一个比现在活动中的任务LEVEL更高的任务,那么在下次任务切换时,就必须无条件地切换到该LEVEL中的该任务去。 326 第 16 天:多任务(2) 此外,如果当前活动中的任务LEVEL被下调,那么此时就必须将其他LEVEL的任务放在优先的位置(同样以上图来说的话,比如当LEVEL 0的任务被降级到LEVEL 2时,任务切换的目标就需要从LEVEL 0变为LEVEL 1) 。 综上所述,我们需要在下次任务切换时先检查LEVEL,因此将lv_change置为1。 接下来是task_sleep,在这里我们可以调用task_remove,因此代码会大大缩短。 本次的mtask.c节选 void task_sleep(struct TASK *task) struct TASK *now_task; if (task-flags = 2) /*如果处于活动状态*/ now_task = task_now(); task_remove(task); /*执行此语句的话flags将变为1 */

      110、 if (task = now_task) /*如果是让自己休眠,则需要进行任务切换*/ task_switchsub(); now_task = task_now(); /*在设定后获取当前任务的值*/ farjmp(0, now_task-sel); return; 这样看上去清清爽爽。 mtask.c的最后是task_switch,除了当lv_change不为0时的处理以外,其余几乎没有变化。 void task_switch(void) struct TASKLEVEL *tl = &taskctl-leveltaskctl-now_lv; struct TASK *new_task, *now_task = tl-taskstl-now; tl-now+; if (tl-now = tl-running) tl-now = 0; if (taskctl-lv_change != 0) task_switchsub(); tl = &taskctl-leveltaskctl-now_lv; new_task = tl-taskstl-now; 5 设定任务优先级(2) (har

      111、ib13e) 327 16 2 3 4 5 8 10 16 9 6 7 12 11 13 15 14 timer_settime(task_timer, new_task-priority); if (new_task != now_task) farjmp(0, new_task-sel); return; 对比前面内容来读的话,应该很容易理解。到此为止,我们对mtask.c的改写就完成了。 fifo.c也需要改写一下,不过和上一节一样,只是将唤醒休眠任务的task_run稍稍修改一下而已。优先级和LEVEL都不需要改变,只要维持原状将任务唤醒即可。 本次的fifo.c节选 int fifo32_put(struct FIFO32 *fifo, int data) /*向FIFO写入数据并累积起来*/ (中略) fifo-free-; if (fifo-task != 0) if (fifo-task-flags != 2) /*如果任务处于休眠状态*/ task_run(fifo-task, -1, 0); /*将任务唤醒*/ return 0; 最后我们来改写HariMain,

      112、可到底该怎么改呢?我们就暂且将任务A设为LEVEL 1, 任务B0B2设为LEVEL 2吧。这样的话,当任务A忙碌的时候就不会切换到任务B0B2,鼠标操作的响应应该会有不小的改善。 本次的bootpack.c节选 void HariMain(void) (中略) init_palette(); shtctl = shtctl_init(memman, binfo-vram, binfo-scrnx, binfo-scrny); task_a = task_init(memman); fifo.task = task_a; task_run(task_a, 1, 0); /*这里! */ (中略) 328 第 16 天:多任务(2) /* sht_win_b */ for (i = 0; i 3; i+) (中略) task_run(task_bi, 2, i + 1); /*这里! */ (中略) 好,我们来“make run” 。画面看上去和harib13d一模一样,但如果用鼠标不停地拖动窗口的话,就会感到响应速度和之前有很大不同。相对地,拖动窗口时任务B0B2会变得非常慢,这就代表我们的设计成功了,撒花! 多任务的基础部分到这里就算完成了。明天我们还会补充一些代码来完善一下,然后就开始制作一些更有操作系统范儿的东西了。大家晚安!

      《30天自制操作系统》由会员桔****分享,可在线阅读,更多相关《30天自制操作系统》请在金锄头文库上搜索。

      点击阅读更多内容
    最新标签
    发车时刻表 长途客运 入党志愿书填写模板精品 庆祝建党101周年多体裁诗歌朗诵素材汇编10篇唯一微庆祝 智能家居系统本科论文 心得感悟 雁楠中学 20230513224122 2022 公安主题党日 部编版四年级第三单元综合性学习课件 机关事务中心2022年全面依法治区工作总结及来年工作安排 入党积极分子自我推荐 世界水日ppt 关于构建更高水平的全民健身公共服务体系的意见 空气单元分析 哈里德课件 2022年乡村振兴驻村工作计划 空气教材分析 五年级下册科学教材分析 退役军人事务局季度工作总结 集装箱房合同 2021年财务报表 2022年继续教育公需课 2022年公需课 2022年日历每月一张 名词性从句在写作中的应用 局域网技术与局域网组建 施工网格 薪资体系 运维实施方案 硫酸安全技术 柔韧训练 既有居住建筑节能改造技术规程 建筑工地疫情防控 大型工程技术风险 磷酸二氢钾 2022年小学三年级语文下册教学总结例文 少儿美术-小花 2022年环保倡议书模板六篇 2022年监理辞职报告精选 2022年畅想未来记叙文精品 企业信息化建设与管理课程实验指导书范本 草房子读后感-第1篇 小数乘整数教学PPT课件人教版五年级数学上册 2022年教师个人工作计划范本-工作计划 国学小名士经典诵读电视大赛观后感诵读经典传承美德 医疗质量管理制度 2 2022年小学体育教师学期工作总结 2022年家长会心得体会集合15篇
    关于金锄头网 - 版权申诉 - 免责声明 - 诚邀英才 - 联系我们
    手机版 | 川公网安备 51140202000112号 | 经营许可证(蜀ICP备13022795号)
    ©2008-2016 by Sichuan Goldhoe Inc. All Rights Reserved.