您好,欢迎光临有路网!
30天自制操作系统【30天从零开始编写图形操作系统】
QQ咨询:
有路璐璐:

30天自制操作系统【30天从零开始编写图形操作系统】

  • 作者:[日] 川合秀实 周自恒 李黎明 曾祥江 张文旭
  • 出版社:人民邮电出版社
  • ISBN:9787115287960
  • 出版日期:2012年08月01日
  • 页数:710
  • 定价:¥99.00
  • 分享领佣金
    手机购买
    城市
    店铺名称
    店主联系方式
    店铺售价
    库存
    店铺得分/总交易量
    发布时间
    操作

    新书比价

    网站名称
    书名
    售价
    优惠
    操作

    图书详情

    内容提要
    自己编写一个操作系统,是许多程序员的梦想。也许有人曾经挑战过,但因为太难而放弃了。其实你错了,你的失败并不是因为编写操作系统太难,而是因为没有人告诉你那其实是一件很简单的事。那么,你想不想再挑战一次呢?
    这是一本兼具趣味性、实用性与学习性的书籍。作者从计算机的构造、汇编语言、C语言开始解说,让你在实践中掌握算法。在这本书的指导下,从零编写所有代码,30天后就可以制作出一个具有窗口系统的32位多任务操作系统。
    本书以课题为主导,边做边玩,抛开晦涩难懂的语言,行文风格十分随性,还充满了各种欢乐的吐槽,适合操作系统爱好者和程序设计人员阅读。
    文章节选
    多任务(1)
    ? 挑战任务切换(harib12a)
    ? 任务切换进阶(harib12b)
    ? 做个简单的多任务(1)(harib12c)
    ? 做个简单的多任务(2)(harib12d)
    ? 提高运行速度(harib12e)
    ? 测试运行速度(harib12f)
    ? 多任务进阶(harib12g)
    1 挑战任务切换(harib12a)
    “话说,多任务到底是啥呢?”我们今天的内容,就从这个问题开始吧。
    多任务,在英语中叫做“multitask”,顾名思义就是“多个任务”的意思。简单地说,在Windows等操作系统中,多个应用程序同时运行的状态(也就是同时打开好几个窗口的状态)就叫做多任务。
    对于生活在现代社会的各位来说,这种多任务简直是理所当然的事情。比如你会一边用音乐播放软件听音乐一边写邮件,邮件写到一半忽然有点东西要查,便打开Web浏览器上网搜索。这对于大家来说这些都是家常便饭了吧。可如果没有多任务的话会怎么样呢?想写邮件的时候就必须关掉正在播放的音乐,要查东西的时候就必须先保存写到一半的邮件,然后才能打开Web浏览器……光想象一下就会觉得太不方便了。
    然而在从前,没有多任务反倒是普遍的情形(那个时候大家不用电脑听音乐,也没有互联网)。在那个年代,电脑一次只能运行一个程序,如果要同时运行多个程序的话,就得买好几台电脑才行。
    就在那个时候,诞生了*初的多任务操作系统,大家都觉得太了不起了。从现在开始,我们也要准备给“纸娃娃系统”添加执行多任务的能力了。连这样一个小不点儿操作系统都能够实现多任务,真是让人不由地感叹它生逢其时呀。
    ■■■■■
    稍稍思考一下我们就会发现,多任务这个东西还真是奇妙,它究竟是怎样做到让多个程序同时运行的呢?如果我们的电脑里面装了好多个CPU的话,同时运行多个程序倒也顺理成章,但实际上就算我们只有一个CPU,照样可以实现多任务。
    其实说穿了,这些程序根本没有在同时运行,只不过看上去好像是在同时运行一样:程序A运行一会儿,接下来程序B运行一会儿,再接下来轮到程序C,然后再回到程序A……如此反复,有点像日本忍者的“分身术”呢(笑)。
    为了让这种分身术看上去更**,需要让操作系统尽可能快地切换任务。如果10秒才切换一次,那就连人眼都能察觉出来了,同时运行多个程序的戏码也就穿帮了。再有,如果我们给程序C发出一个按键指令,正巧这个瞬间系统切换到了程序A的话,我们就不得不等上20秒,才能重新轮到程序C对按键指令作出反应。这实在是让人抓狂啊(哭)。
    在一般的操作系统中,这个切换的动作每0.01~0.03秒就会进行一次。当然,切换的速度越快,让人觉得程序是在同时运行的效果也就越好。不过,CPU进行程序切换(我们称为“任务切换”)这个动作本身就需要消耗一定的时间,这个时间大约为0.0001秒左右,不同的CPU及操作系统所需的时间也有所不同。如果CPU每0.0002秒切换一次任务的话,该CPU处理能力的50%都要被任务切换本身所消耗掉。这意味着,如果同时运行2个程序,每个程序的速度就只有单独运行时的1/4,这样你会觉得开心吗?如果变成这种结果,那还不如干脆别搞多任务呢。
    相比之下,即便是每0.001秒切换一次任务,单单在任务切换上面也要消耗CPU处理能力的10%。大概有人会想,10%也没什么大不了的吧?可如果你看看速度快10%的CPU卖多少钱,说不定就会恍然大悟,“对啊,只要优化一下任务切换间隔,就相当于一分钱也不花,便换上了比现在更快的CPU嘛……”(笑),你也就明白了浪费10%也是很不值得的。正是因为这个原因,任务切换的间隔*短也得0.01秒左右,这样一来只有1%的处理能力消耗在任务切换上,基本上就可以忽略不计了。
    ■■■■■
    关于多任务是什么的问题,已经大致讲得差不多了,接下来我们来看看如何让CPU来处理多任务。
    当你向CPU发出任务切换的指令时,CPU会先把寄存器中的值全部写入内存中,这样做是为了当以后切换回这个程序的时候,可以从中断的地方继续运行。接下来,为了运行下一个程序,CPU会把所有寄存器中的值从内存中读取出来(当然,这个读取的地址和刚刚写入的地址一定是不同的,不然就相当于什么都没变嘛),这样就完成了一次切换。我们前面所说的任务切换所需要的时间,正是对内存进行写入和读取操作所消耗的时间。
    ■■■■■
    接下来我们来看看寄存器中的内容是怎样写入内存里去的。下面这个结构叫做“任务状态段”(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为止的几个成员,保存的不是寄存器的数据,而是与任务设置相关的信息,在执行任务切换的时候这些成员不会被写入(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记忆中下一条该执行的指令的地址,蒙了CPU一把。这样一来,CPU在读取下一条指令时,就会去读取0x1234这个地址中的指令。你看,这不就相当于是做了一个跳转吗?
    对了,如果你在汇编语言里用MOV EIP,0x1234这种写法是会出错的,还是不要尝试的好。在汇编语言中,应该使用JMP 0x1234来代替MOV EIP,0x1234。
    如果在TSS中将EIP寄存器的值记录下来,那么当下次再返回这个任务的时候,CPU就可以明白应该从哪里读取程序来运行了。
    按照常识,段寄存器应该是16位的才对,可是在TSS数据结构中却定义成了int(也就是DWORD)类型。我们可以大胆想象一下,说不定英特尔公司的人将来会把段寄存器变成32位的,这样想想也挺有意思的呢(笑)。
    第4行的ldtr和iomap也和第1行的成员一样,是有关任务设置的部分,因此在任务切换时不会被CPU写入。也许你会想,那就和第1行一样,暂时先忽略好了——但那可是**不行的!如果胡乱赋值的话,任务就无法正常切换了,在这里我们先将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每次执行带有段地址的指令时,都会去确认一下GDT中的设置,以便判断接下来要执行的JMP指令到底是普通的far-JMP,还是任务切换。也就是说,从汇编程序翻译出来的机器语言来看,普通的far-JMP和任务切换的far-JMP,指令本身是没有任何区别的。
    ■■■■■
    好了,枯燥的讲解就到这里,让我们实际做一次任务切换吧。我们准备两个任务:任务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 + 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); 本次的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-JMP指令是用作任务切换的话,地址段(冒号前面的4*8的部分)要指向TSS这一点比较重要,而偏移量(冒号后面的0的部分)并没有什么实际作用,会被忽略掉,一般来说像这样写0就可以了。
    现在我们需要在HariMain的某个地方来调用taskswitch(),可到底该写在哪里呢?唔,有了,就放在显示“10[sec]”的语句后面好了。也就是说,程序启动10秒以后进行任务切换。
    本次的HariMain节选
    } else if (i == 10) { /* 10秒计时器*/
    putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 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; */
    tss_b.eax = 0;
    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赋给变量的话,该变量的值就显示为0x00000202,因此在这里就直接使用了这个值,仅此而已。如果还有看不懂的地方,大概就是eip和esp的部分了吧。
    ■■■■■
    在eip中,我们需要定义在切换到这个任务的时候,要从哪里开始运行。在这里我们先把task_b_main这个函数的内存地址赋值给它。
    本次的bootpack.c节选
    void task_b_main(void)
    {
    for (;;) { io_hlt(); }
    }
    这个函数只执行了一个HLT,没有任何实际作用,后面我们会对它进行各种改造,现在就先这样吧。
    ■■■■■
    task_b_esp是专门为任务B所定义的栈。有人可能会说,直接用任务A的栈不就好了吗?那可不行,如果真这么做的话,栈就会混成一团,程序也无法正常运行。 本次的HariMain节选
    int task_b_esp; task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024;
    总之先写成这个样子了。我们为任务B的栈分配了64KB的内存,并计算出栈底的内存地址。请各位回忆一下向栈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就可以了。
    本次的bootpack.c节选
    void task_b_main(void)
    {
    struct FIFO32 fifo;
    struct TIMER *timer;
    int i, fifobuf[128]; 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中的对应变量。这并不是因为它们处于不同的任务,而是因为它们名字虽然一样,但实际上根本是不同的变量(之前一直没有机会解释这一点,现在稍微晚了点,不过还是在这里讲一下吧)。
    对了,taskswitch3还没有创建,我们需要创建它。
    本次的naskfunc.nas节选
    _taskswitch3: ; void taskswitch3(void);
    JMP 3*8:0
    RET
    好了,准备完毕!
    ■■■■■
    我们来“make run”一下。哇,经过10秒之后光标停止闪烁,鼠标没有反应,键盘也无法输入文字了。然而又过了5秒,光标又重新开始闪烁,刚才键盘没反应的时候打进去的字一口气全都冒了出来,鼠标也又能动了。
    这就说明我们已经成功回到了任务A并继续运行了,真顺利呀。
    3 做个简单的多任务(1)(harib12c)
    接下来,我们要实现更快速的,而且是来回交替的任务切换。这样一来,我们就可以告别光标停住、鼠标卡死、键盘打不了字等情况,从而让两个任务看上去好像在同时运行一样。
    在开始动手之前,笔者认为像taskswitch3、taskswitch4这种写法实在不好。假设我们有100个任务,难道就要创建100个任务切换函数不成?这样肯定不行,*好是写成一个函数,比如像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”的缩写,代表“任务切换计时器”的意思。
    本次的bootpack.c节选
    void HariMain(void)
    {
    (中略) timer_ts = timer_alloc();
    timer_init(timer_ts, &fifo, 2);
    timer_settime(timer_ts, 2); (中略) for (;;) {
    io_cli();
    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, "10[sec]", 7);
    } else if (i == 3) { /* 3秒计时器*/
    putfonts8_asc_sht(sht_back, 0, 80, COL8_FFFFFF, COL8_008484, "3[sec]", 6);
    } else if (i <= 1) { /*光标用计时器*/
    (中略)
    }
    }
    }
    } void task_b_main(void)
    {
    struct FIFO32 fifo;
    struct TIMER *timer_ts;
    int i, fifobuf[128]; fifo32_init(&fifo, 128, fifobuf);
    timer_ts = timer_alloc();
    timer_init(timer_ts, &fifo, 1);
    timer_settime(timer_ts, 2); for (;;) {
    io_cli();
    if (fifo32_status(&fifo) == 0) {
    io_stihlt();
    } else {
    i = fifo32_get(&fifo);
    io_sti();
    if (i == 1) { /*任务切换*/
    farjmp(0, 3 * 8);
    timer_settime(timer_ts, 2);
    }
    }
    }
    }
    上面的代码应该没有什么难点,不过还是稍微解释一下吧。在每个任务中,当从farjmp返回的时候,我们都将计时器重新设定到0.02秒之后,以便让程序在返回0.02秒之后,再次执行任务切换。
    ■■■■■
    好了,这样做是不是能像我们所设想的那样,让键盘和鼠标持续响应呢?我们来“make run”……不错,键盘打字、鼠标操作、光标闪烁,全都运行正常,完全没有卡住。我们成功了。
    不过,我们真的成功了吗?感觉不是很靠谱啊,task_b_main到底有没有运行啊?嗯,下面我们想办法来确认一下。
    4 做个简单的多任务(2)(harib12d)
    为了确认task_b_main到底有没有运行,我们需要让task_b_main显示点什么东西出来,*好是显示点会动的东西,要不还是让它数数吧……喂喂,是谁在下面叫“又来了啊”?(笑)
    本次的bootpack.c节选
    void task_b_main(void)
    {
    struct FIFO32 fifo;
    struct TIMER *timer_ts;
    int i, fifobuf[128], count = 0;
    char s[11];
    struct SHEET *sht_back; (中略) for (;;) {
    count++;
    sprintf(s, "%10d", count);
    putfonts8_asc_sht(sht_back, 0, 144, COL8_FFFFFF, COL8_008484, s, 10);
    io_cli();
    if (fifo32_status(&fifo) == 0) {
    io_sti();
    } else {
    (中略)
    }
    }
    }
    写到这里,我们遇到了一个问题,那就是sht_back。HariMain知道这个变量的值,但task_b_main可不知道。怎么办呢?怎样才能把这个变量的值从任务A传递给任务B呢?随便找一个内存地址存进去,然后再从那里读出来,这样应该可以吧。好,就用0x0fec这个地址,这个地址是BOOTINFO?4。
    本次的HariMain节选
    *((int *) 0x0fec) = (int) sht_back;
    本次的task_b_main节选
    sht_back = (struct SHEET *) *((int *) 0x0fec);
    这里用了很多强制数据类型转换操作,代码比较难读,不过就先这样吧。
    ■■■■■
    现在让我们来运行一下。不知道结果如何,心里好紧张啊。“make run”,哇,动了动了!task_b_main和HariMain在同时运行!当然,实际上只是因为切换速度很快,所以造成了在同时运行的假象。无论如何,我们的多任务取得了圆满成功!

    多任务成功
    (其实我们在harib12c的时候就已经成功实现了多任务,只不过当时还没有加入显示功能,所以无法实际感受到而已。)
    5 提高运行速度(harib12e)
    刚开始看到harib12d的成果还觉得挺感动的,过段时间头脑冷静下来以后再看的话,发现task_b_main数数的速度即便在真机环境下运行还是非常慢,我们得想办法提高它的运行速度。Harib10i在7秒钟的时间内可以数到0099969264,相比之下,harib12d也太慢了。任务A和任务B交替运行的情况下,性能下降到原来的一半还可以理解,如果比这个还慢的话就让人无法忍受了。
    那运行速度为什么会这么慢呢?因为我们的程序每计1个数就在画面上显示一次,但1秒钟之内刷新100次以上的话,人眼根本就分辨不出来,所以我们不需要计1个数就刷新一次,只要每隔0.01秒刷新一次就足够了。
    本次的bootpack.c节选
    void task_b_main(struct SHEET *sht_back)
    {
    struct FIFO32 fifo;
    struct TIMER *timer_ts, *timer_put;
    int i, fifobuf[128], count = 0;
    char s[12]; fifo32_init(&fifo, 128, fifobuf);
    timer_ts = timer_alloc();
    timer_init(timer_ts, &fifo, 2);
    timer_settime(timer_ts, 2);
    timer_put = timer_alloc();
    timer_init(timer_put, &fifo, 1);
    timer_settime(timer_put, 1); 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 == 2) {
    farjmp(0, 3 * 8);
    timer_settime(timer_ts, 2);
    }
    }
    }
    }
    基本上就是这个样子。对了,代码开头的sht_back我们改为作为函数的参数来传递了,关于这一点我们以后会讲到,大家不必担心。
    另外,上面的代码还把任务切换计时器超时的时候向FIFO写入的值改为了2。其实不改也没什么问题,只不过因为这个计时器定了0.02秒这个数,所以就顺手改成2了。
    还有,count数值的显示格式改成了11位数字,因为运行速度变快了的话,说不定数字位数会不够用呢(笑)。
    ■■■■■
    关于将sht_back的值从HariMain传递过来的方法,*((int *) 0x0fec)这样的写法感觉实在是不好看,于是果断废弃了,我们用栈来替代它。
    举个例子,load_tr(123);这样的函数调用,如果从汇编语言的角度来考虑的话,参数指定的数值(123)就放在内存中,地址为ESP+4,这是C语言的一个既定机制。
    既然有这种机制,那么我们可以反过来利用一下,也就是说,在HariMain里面这样写:
    本次的HariMain节选
    task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8; *((int *) (task_b_esp + 4)) = (int) sht_back;
    这样一来,在任务B启动的时候,[ESP+4]这个地址里面就已经存入了sht_back的值,因此我们就欺骗了task_b_main,让它以为自己所接收到的sht_back是作为一个参数传递过来的。
    可能有人不明白为什么我们要把task_b_esp的地址减8,减4不就可以了吗?我们当然不能减4,只要仔细思考一下就能搞清楚这里的奥妙。
    假设memman_alloc_4k分配出来的内存地址为0x01234000,由于我们申请分配了64KB的内存空间,那么我们可以自由使用的内存地址就是从0x01234000到0x01243fff为止的这一块。如果在这里我们既不减4也不减8,而是直接加上64 * 1024的话,task_b_esp即为0x01244000。如果我们减去4,task_b_esp即为0x01243ffc,但我们写入sht_back的地址是task_b_esp + 4,算下来就变成了0x01244000,如果把4字节的sht_back值写入这个地址的话,就超出了分配给我们的内存范围(0x01234000~0x01243fff),这样不行。
    而如果我们减去8,task_b_esp即为0x01243ff8,写入sht_back的地址是task_b_esp + 4,即0x01243ffc,从这个地址向后写入4字节的sht_back值,则正好在分配出来的内存范围(0x01234000~
    0x01243fff)内完成操作,这样就不会出问题了。
    ■■■■■
    好,我们来运行一下,看看是不是变快了?还有,task_b_main有没有被我们欺骗而顺利接收到sht_back的值呢?如果这一招不成功的话,sht_back的值就会出现异常,画面上也就应该显示不出数字了。“make run”,哇,成功了,而且速度飞快(请注意,右图显示的是程序还没有运行到10秒的状态,这时就已经数到这么大的数字了!)。

    即便是模拟器环境下运行速度也已经相当快了
    COLUMN-9 千万不能return?
    在这一节中,task_b_main已经变得像一个普通函数一样了,但是在这个函数中千万不能使用return。
    return的功能,说到底其实是返回函数被调用位置的一个JMP指令,但这个task_b_main并不是由某段程序直接调用的,因此不能使用return。如果强行return的话,就会像“执行数据”一样发生问题,程序无法正常运行。
    HariMain的情况也是一样的,也禁止使用return。
    我们在15.1节中讲过,为了记住现在正在执行的指令所在的内存地址,需要使用EIP寄存器,那么return的时候要返回的地址又记录在哪里呢?对于记性不好的CPU来说,肯定会把这个地址保存在某个地方,没错,它就保存在栈中,地址是[ESP]。
    因此,我们不仅可以利用[ESP+4],还可以利用[ESP]来欺骗CPU,其实只要向[ESP]写入一个合适的值,告诉CPU应该返回到哪个地址,task_b_main中就可以使用return了。
    6 测试运行速度(harib12f)
    我们的程序运行得很快,可是到底有多快呢?我们得想个办法测一下。可能有人会说,别搞这种节外生枝的玩意儿了,赶快继续往下讲吧!嗯,要说也是,这的确是有点不务正业了,不过该玩的时候也要玩一玩嘛!
    我们向task_b_main添加了一些代码。
    本次的bootpack.c节选
    void task_b_main(struct SHEET *sht_back)
    {
    struct FIFO32 fifo;
    struct TIMER *timer_ts, *timer_put, *timer_1s;
    int i, fifobuf[128], count = 0, count0 = 0;
    char s[12]; (中略)
    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) {
    (中略)
    } else if (i == 2) {
    (中略)
    } else if (i == 100) {
    sprintf(s, "%11d", count - count0);
    putfonts8_asc_sht(sht_back, 0, 128, COL8_FFFFFF, COL8_008484, s, 11);
    count0 = count;
    timer_settime(timer_1s, 100);
    }
    }
    }
    }
    ■■■■■
    我们来运行一下,先得看看它是不是能正常工作,“make run”。不错,在模拟器环境下运行成功。

    上面的数字显示的是速度哦
    ■■■■■
    现在我们到真机环境运行一下。哇,好快!果然真机环境就是快,速度已经达到大约4638200 了。
    我们把这个速度和harib10i做个对比。harib10i在7秒内计数到0099969264,即速度为每秒14281323,相比之下性能是现在的3倍。咦,怎么会这样?如果是2倍的话还可以理解,3倍就有点过分了。
    看到这个结果心里当然会很不爽,我们来找找原因。嗯,每隔0.01秒刷新显示是不是不太好呢?如果显示计数是导致速度慢的原因,那干脆就别显示了吧。我们把开头的timer_settime (timer_put, 1);一句删掉,这样一来由于计时器没有被设定,就不会引起超时中断,也就不会触发显示了。

    现在仅显示速度值了
    那么在真机环境下运行情况如何呢?哇,速度真的快了不少,现在的成绩是6774100,和14281323相比,性能差距为2.1倍,这样已经很令人满意了。大概JMP的地址也会影响计数的速度,另外,如果把速度显示改成每隔5秒刷新一次,任务切换间隔再改成0.03秒的话,估计性能差距可以更加接近理想的2.0倍,不过现在这个阶段我们就不去一一尝试了。
    7 多任务进阶(harib12g)
    到现在为止,我们所做的多任务都是依靠在HariMain和task_b_main中写入负责任务切换的代码来实现的。有人会说,这种多任务方式“不是真正的多任务”(即便如此,应该也不至于被说成是“假的”多任务)。
    那么真正的多任务又是什么样的呢?真正的多任务,是要做到在程序本身不知道的情况下进行任务切换。既然如此,我们就来为“纸娃娃系统”添加真正的多任务吧。
    首先我们来创建这样一个函数。
    本次的mtask.c节选
    struct TIMER *mt_timer;
    int mt_tr; void mt_init(void)
    { mt_timer = timer_alloc();
    /*这里没有必要使用timer_init */
    timer_settime(mt_timer, 2);
    mt_tr = 3 * 8;
    return;
    } void mt_taskswitch(void)
    {
    if (mt_tr == 3 * 8) {
    mt_tr = 4 * 8;
    } else {
    mt_tr = 3 * 8;
    }
    timer_settime(mt_timer, 2);
    farjmp(0, mt_tr);
    return;
    } mt_init函数的功能是初始化mt_timer和mt_tr的值,并将计时器设置为0.02秒之后,仅此而已。在这里,变量mt_tr实际上代表了TR寄存器,而不需要使用timer_init是因为在发生超时的时候不需要向FIFO缓冲区写入数据。具体内容请继续往下看。
    接下来,mt_taskswitch函数的功能是按照当前的mt_tr变量的值计算出下一个mt_tr的值,将计时器重新设置为0.02秒之后,并进行任务切换,很简单吧。
    ■■■■■
    下面我们来改造一下timer.c的inthandler20。
    本次的timer.c节选
    void inthandler20(int *esp)
    {
    char ts = 0;
    (中略)
    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 {
    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。*后判断如果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;
    }
    为什么不这样写呢?这样写的确可以让代码更简短,但是会出问题。
    出问题的原因在于,调用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;
    int i, fifobuf[128], count = 0, count0 = 0;
    char s[12]; fifo32_init(&fifo, 128, fifobuf);
    timer_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, 128, COL8_FFFFFF, COL8_008484, s, 11);
    count0 = count;
    timer_settime(timer_1s, 100);
    }
    }
    }
    }
    像这样,把有关任务切换的部分全部删掉就可以了。
    ■■■■■
    好,我们来试试看能不能正常工作吧。“make run”,成功了,真开心!不过看上去和之前没什么区别。
    和上一节相比,为什么现在的设计可以称为“真正的多任务”呢?因为如果使用这样的设计,即便在程序中不进行任务切换的处理(比如忘记写了,或者因为bug没能正常切换之类的),也一定会正常完成切换。之前那种多任务的话,如果任务B因为发生bug而无法进行切换,那么当切换到任务B以后,其他的任务就再也无法运行了,这样会造成无论是按键盘还是动鼠标都毫无反应的悲剧。

    真正的多任务也成功了!
    真正的多任务不会发生这样的问题,因此这种方式更好……话虽如此,但其实即便是harib12g,在任务B发生bug的情况下,也有可能出现键盘输入失去响应的问题。例如,明明写了io_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来说负担更小些,一定是这个原因吧。
    哎呀,不知不觉就已经很晚了。今天就先到这里吧,我们明天继续。
    目录
    第0天 着手开发之前 1
    1 前言 1
    2 何谓操作系统 3
    3 开发操作系统的各种方法 4
    4 无知则无畏 4
    5 如何开发操作系统 6
    6 操作系统开发中的困难 7
    7 学习本书时的注意事项(重要!) 9
    8 各章内容摘要 11
    第1天 从计算机结构到汇编程序入门 13
    1 先动手操作 13
    2 究竟做了些什么 19
    3 初次体验汇编程序 22
    4 加工润色 24
    第2天 汇编语言学习与Makefile入门 28
    1 介绍文本编辑器 28
    2 继续开发 29
    3 先制作启动区 40
    4 Makefile入门 41
    第3天 进入32位模式并导入C语言 45
    1 制作真正的IPL 45
    2 试错 50
    3 读到18扇区 51
    4 读入10个柱面 52
    5 着手开发操作系统 54
    6 从启动区执行操作系统 55
    7 确认操作系统的执行情况 56
    8 32位模式前期准备 57
    9 开始导入C语言 59
    10 实现HLT(harib00j) 62
    第4天 C语言与画面显示的练习 64
    1 用C语言实现内存写入(harib01a) 64
    2 条纹图案(harib01b) 67
    3 挑战指针(harib01c) 69
    4 指针的应用(1)(harib01d) 74
    5 指针的应用(2)(harib01e) 74
    6 色号设定(harib01f) 75
    7 绘制矩形(harib01g) 84
    8 今天的成果(harib01h) 86
    第5天 结构体、文字显示与GDT/IDT初始化 88
    1 接收启动信息(harib02a) 88
    2 **结构体(harib02b) 89
    3 **箭头记号(harib02c) 91
    4 显示字符(harib02d) 91
    5 增加字体(harib02e) 94
    6 显示字符串(harib02f) 96
    7 显示变量值(harib02g) 97
    8 显示鼠标指针(harib02h) 99
    9 GDT与IDT的初始化(harib02i) 101
    第6天 分割编译与中断处理 108
    1 分割源文件(harib03a) 108
    2 整理Makefile(harib03b) 109
    3 整理头文件(harib03c) 110
    4 意犹未尽 112
    5 初始化PIC(harib03d) 115
    6 中断处理程序的制作(harib03e) 119
    第7天 FIFO与鼠标控制 125
    1 获取按键编码(hiarib04a) 125
    2 加快中断处理(hiarib04b) 127
    3 制作FIFO缓冲区(hiarib04c) 130
    4 改善FIFO缓冲区(hiarib04d) 133
    5 整理FIFO缓冲区(hiarib04e) 135
    6 总算讲到鼠标了(harib04f) 138
    7 从鼠标接受数据(harib04g) 141
    第8天 鼠标控制与32位模式切换 144
    1 鼠标解读(1)(harib05a) 144
    2 稍事整理(harib05b) 146
    3 鼠标解读(2)(harib05c) 148
    4 移动鼠标指针(harib05d) 151
    5 通往32位模式之路 153
    第9天 内存管理 162
    1 整理源文件(harib06a) 162
    2 内存容量检查(1)(harib06b) 163
    3 内存容量检查(2)(harib06c) 168
    4 挑战内存管理(harib06d) 172
    第10天 叠加处理 181
    1 内存管理(续)(harib07a) 181
    2 叠加处理(harib07b) 184
    3 提高叠加处理速度(1)(harib07c) 194
    4 提高叠加处理速度(2)(harib07d) 197
    第11天 制作窗口 201
    1 鼠标显示问题(harib08a) 201
    2 实现画面外的支持(harib08b) 202
    3 shtctl的指定省略(harib08c) 203
    4 显示窗口(harib08d) 206
    5 小实验(harib08e) 208
    6 高速计数器(harib08f) 209
    7 消除闪烁(1)(harib08g) 211
    8 消除闪烁(2)(harib08h) 214
    第12天 定时器(1) 220
    1 使用定时器(harib09a) 220
    2 计量时间(harib09b) 224
    3 超时功能(harib09c) 225
    4 设定多个定时器(harib09d) 228
    5 加快中断处理(1)(harib09e) 232
    6 加快中断处理(2)(harib09f) 234
    7 加快中断处理(3)(harib09g) 236
    第13天 定时器(2) 240
    1 简化字符串显示(harib10a) 240
    2 重新调整FIFO缓冲区(1)(harib10b) 241
    3 测试性能(harib10c~harib10f) 243
    4 重新调整FIFO缓冲区(2)(harib10g) 246
    5 加快中断处理(4)(harib10h) 253
    6 使用“哨兵”简化程序(harib10i) 257
    第14天 高分辨率及键盘输入 262
    1 继续测试性能(harib11a~harib11c) 262
    2 提高分辨率(1)(harib11d) 266
    3 提高分辨率(2)(harib11e) 269
    4 键盘输入(1)(harib11f) 272
    5 键盘输入(2)(harib11g) 275
    6 追记内容(1)(harib11h) 277
    7 追记内容(2)(harib11i) 279
    第15天 多任务(1) 282
    1 挑战任务切换(harib12a) 282
    2 任务切换进阶(harib12b) 289
    3 做个简单的多任务(1)(harib12c) 291
    4 做个简单的多任务(2)(harib12d) 293
    5 提高运行速度(harib12e) 294
    6 测试运行速度(harib12f) 297
    7 多任务进阶(harib12g) 299
    第16天 多任务(2) 304
    1 任务管理自动化(harib13a) 304
    2 让任务休眠(harib13b) 308
    3 增加窗口数量(harib13c) 313
    4 设定任务优先级(1)(harib13d) 317
    5 设定任务优先级(2)(harib13e) 320
    第17天 命令行窗口 329
    1 闲置任务(harib14a) 329
    2 创建命令行窗口(harib14b) 331
    3 切换输入窗口(harib14c) 334
    4 实现字符输入(harib14d) 337
    5 符号的输入(harib14e) 341
    6 大写字母与小写字母(harib14f) 343
    7 对各种锁定键的支持(harib14g) 346
    第18天 dir命令 350
    1 控制光标闪烁(1)(harib15a) 350
    2 控制光标闪烁(2)(harib15b) 352
    3 对回车键的支持(harib15c) 355
    4 对窗口滚动的支持(harib15d) 357
    5 mem命令(harib15e) 359
    6 cls命令(harib15f) 363
    7 dir命令(harib15g) 366
    第19天 应用程序 371
    1 type命令(harib16a) 371
    2 type命令改良(harib16b) 378
    3 对FAT的支持(harib16c) 382
    4 代码整理(harib16d) 387
    5 **个应用程序(harib16e) 387
    第20天 API 392
    1 程序整理(harib17a) 392
    2 显示单个字符的API(1)(harib17b) 399
    3 显示单个字符的API(2)(harib17c) 402
    4 结束应用程序(harib17d) 403
    5 不随操作系统版本而改变的API(harib17e) 405
    6 为应用程序自由命名(harib17f) 408
    7 当心寄存器(harib17g) 410
    8 用API显示字符串(harib17h) 412
    第21天 保护操作系统 418
    1 攻克难题——字符串显示API(harib18a) 418
    2 用C语言编写应用程序(harib18b) 420
    3 保护操作系统(1)(harib18c) 424
    4 保护操作系统(2)(harib18d) 426
    5 对异常的支持(harib18e) 431
    6 保护操作系统(3)(harib18f) 434
    7 保护操作系统(4)(harib18g) 435
    第22天 用C语言编写应用程序 443
    1 保护操作系统(5)(harib19a) 443
    2 帮助发现bug(harib19b) 448
    3 强制结束应用程序(harib19c) 452
    4 用C语言显示字符串(1)(harib19d) 455
    5 用C语言显示字符串(2)(harib19e) 457
    6 显示窗口(harib19f) 462
    7 在窗口中描绘字符和方块(harib19g) 465
    第23天 图形处理相关 468
    1 编写malloc(harib20a) 468
    2 画点(harib20b) 472
    3 刷新窗口(harib20c) 475
    4 画直线(harib20d) 478
    5 关闭窗口(harib20e) 483
    6 键盘输入API(harib20f) 484
    7 用键盘输入来消遣一下(harib20g) 488
    8 强制结束并关闭窗口(harib20h) 489
    第24天 窗口操作 493
    1 窗口切换(1)(harib21a) 493
    2 窗口切换(2)(harib21b) 495
    3 移动窗口(harib21c) 496
    4 用鼠标关闭窗口(harib21d) 498
    5 将输入切换到应用程序窗口(harib21e) 500
    6 用鼠标切换输入窗口(harib21f) 506
    7 定时器API(harib21g) 507
    8 取消定时器(harib21h) 511
    第25天 增加命令行窗口 515
    1 蜂鸣器发声(harib22a) 515
    2 增加更多的颜色(1)(harib22b) 518
    3 增加更多的颜色(2)(harib22c) 520
    4 窗口初始位置(harib22d) 523
    5 增加命令行窗口(1)(harib22e) 524
    6 增加命令行窗口(2)(harib22f) 528
    7 增加命令行窗口(3)(harib22g) 531
    8 增加命令行窗口(4)(harib22h) 532
    9 变得更像真正的操作系统(1)(harib22i) 534
    10 变得更像真正的操作系统(2)(harib22j) 538
    第26天 为窗口移动提速 541
    1 提高窗口移动速度(1)(harib23a) 541
    2 提高窗口移动速度(2)(harib23b) 543
    3 提高窗口移动速度(3)(harib23c) 547
    4 提高窗口移动速度(4)(harib23d) 549
    5 启动时只打开一个命令行窗口(harib23e) 551
    6 增加更多的命令行窗口(harib23f) 554
    7 关闭命令行窗口(1)(harib23g) 555
    8 关闭命令行窗口(2)(harib23h) 561
    9 start命令(harib23i) 563
    10 ncst命令(harib23j) 564
    第27天 LDT与库 571
    1 先来修复bug(harib24a) 571
    2 应用程序运行时关闭命令行窗口(harib24b) 573
    3 保护应用程序(1)(harib24c) 577
    4 保护应用程序(2)(harib24d) 580
    5 优化应用程序的大小(harib24e) 583
    6 库(harib24f) 587
    7 整理make环境(harib24g) 590
    第28天 文件操作与文字显示 598
    1 alloca(1)(harib25a) 598
    2 alloca(2)(harib25b) 601
    3 文件操作API(harib25c) 605
    4 命令行API(harib25d) 612
    5 日文文字显示(1)(harib25e) 615
    6 日文文字显示(2)(harib25f) 624
    7 日文文字显示(3)(harib25g) 629
    第29天 压缩与简单的应用程序 635
    1 修复bug(harib26a) 635
    2 文件压缩(harib26b) 636
    3 标准函数 644
    4 非矩形窗口(harib26c) 647
    5 bball(harib26d) 648
    6 外星人游戏(harib26e) 651
    第30天 **的应用程序 659
    1 命令行计算器(harib27a) 659
    2 文本阅览器(harib27b) 664
    3 MML播放器(harib27c) 671
    4 图片阅览器(harib27d) 679
    5 IPL的改良(harib27e) 683
    6 光盘启动(harib27f) 688
    第31天 写在开发完成之后 690
    1 继续开发要靠大家的努力 690
    2 关于操作系统的大小 692
    3 操作系统开发的诀窍 693
    4 分享给他人使用 694
    5 关于光盘中的软件 695
    6 关于开源的建议 696
    7 后记 698
    8 毕业典礼 703
    9 附录 704
    编辑推荐语
    只需30天
    从零开始编写一个五脏俱全的图形操作系统
    39.1KB迷你系统
    实现多任务、汉字显示、文件压缩,还能听歌看图玩游戏
    日本编程天才
    揭开CPU、内存、磁盘以及操作系统底层工作模式的神秘面纱

    与描述相符

    100

    北京 天津 河北 山西 内蒙古 辽宁 吉林 黑龙江 上海 江苏 浙江 安徽 福建 江西 山东 河南 湖北 湖南 广东 广西 海南 重庆 四川 贵州 云南 西藏 陕西 甘肃 青海 宁夏 新疆 台湾 香港 澳门 海外