出版日期:2012年08月
ISBN:9787115287960
[十位:7115287961]
页数:710
定价:¥99.00
店铺售价:¥39.60
(为您节省:¥59.40)
店铺库存:0
本
正在处理购买信息,请稍候……
我要买:
本
* 如何购买
联系店主:
18924022418
《30天自制操作系统【30天从零开始编写图形操作系统】》新旧程度及相关说明:
50 45
店主推荐图书:
-
¥11.20
-
¥7.20
-
¥10.00
-
¥9.10
-
100分
满分
确认收货后30天未评价,系统默认好评!
[2023-05-04 15:49:49]
迟**
济南市
-
100分
满分
确认收货后30天未评价,系统默认好评!
[2023-03-25 09:57:34]
白*
广州市
-
100分
满分
确认收货后30天未评价,系统默认好评!
[2023-03-22 22:22:07]
武**
襄阳市
-
100分
满分
确认收货后30天未评价,系统默认好评!
[2023-03-19 20:59:27]
罗**
上海市
-
100分
满分
确认收货后30天未评价,系统默认好评!
[2022-12-26 09:10:29]
韩**
儋州市
《30天自制操作系统【30天从零开始编写图形操作系统】》内容提要:
自己编写一个操作系统,是许多程序员的梦想。也许有人曾经挑战过,但因为太难而放弃了。其实你错了,你的失败并不是因为编写操作系统太难,而是因为没有人告诉你那其实是一件很简单的事。那么,你想不想再挑战一次呢?
这是一本兼具趣味性、实用性与学习性的书籍。作者从计算机的构造、汇编语言、C语言开始解说,让你在实践中掌握算法。在这本书的指导下,从零编写所有代码,30天后就可以制作出一个具有窗口系统的32位多任务操作系统。
本书以课题为主导,边做边玩,抛开晦涩难懂的语言,行文风格十分随性,还充满了各种欢乐的吐槽,适合操作系统爱好者和程序设计人员阅读。
《30天自制操作系统【30天从零开始编写图形操作系统】》图书目录:
第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天自制操作系统【30天从零开始编写图形操作系统】》文章节选:
多任务(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来说负担更小些,一定是这个原因吧。
哎呀,不知不觉就已经很晚了。今天就先到这里吧,我们明天继续。
《30天自制操作系统【30天从零开始编写图形操作系统】》编辑推荐与评论:
只需30天
从零开始编写一个五脏俱全的图形操作系统
39.1KB迷你系统
实现多任务、汉字显示、文件压缩,还能听歌看图玩游戏
日本编程天才
揭开CPU、内存、磁盘以及操作系统底层工作模式的神秘面纱
《30天自制操作系统【30天从零开始编写图形操作系统】》作者介绍:
川合秀实(Hidemi Kawai)
生于1975年,是一位以“轻量化”编程思想见长的“非主流”***。2000年因自行开发的OSASK项目而名声大噪。OSASK是一个开源的32位微型操作系统,它并非以Linux等内核为基础,而是完全从零开始开发,在一张软盘的容量下实现了GUI、多任务、多语言等**特性,启动时间只需1秒。本书的内容可以看成是作者以OSASK为蓝本,教会读者从零开始开发一个操作系统,同时可以让初学者在编写操作系统的过程中,了解操作系统背后更多的知识。