只需30天
从零开始编写一个五脏俱全的图形操作系统
39.1KB迷你系统
实现多任务、汉字显示、文件压缩,还能听歌看图玩游戏
日本编程天才
揭开CPU、内存、磁盘以及操作系统底层工作模式的神秘面纱
第15天
多任务(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中进行定义后使用。
参考上面的结构定义,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秒钟的等待还真是漫长……哇!停了停了!
看来我们的首次任务切换获得了圆满成功。
……
我是一个对技术有着狂热追求的业余爱好者,一直梦想着能够亲手构建一个属于自己的操作系统。然而,这条路充满了挑战,各种知识点像迷雾一样笼罩着我。《30天自制操作系统》这本书,就像一束光,照亮了我前行的道路。它的语言风格非常接地气,没有过多的学术术语堆砌,而是用一种能够被普通读者理解的方式来解释复杂的概念。书中的每一个步骤都经过精心设计,能够引导读者逐步攻克难关。我最喜欢的部分是关于如何设计和实现内核的章节,作者用非常巧妙的比喻和图示,将原本抽象的内核工作原理变得生动形象。光盘里附带的源代码,更是让我爱不释手,我可以直接在虚拟机上运行,观察它的行为,并尝试进行修改,看看不同的参数设置会带来什么结果。通过这本书,我不仅学习到了操作系统的基本原理,还培养了解决实际问题的能力,更重要的是,我感受到了创造的乐趣。这本书绝对是每个有志于自制操作系统的人必不可少的伙伴。
评分我是一名已经工作多年的软件工程师,虽然日常工作接触的是应用层开发,但一直对操作系统的底层细节感到好奇,总觉得不把这一块弄明白,对整个计算机体系的理解就不够透彻。市面上有些关于操作系统的书,动辄几百页,内容又晦涩难懂,看了几页就容易打退堂鼓。这本《30天自制操作系统》的出现,恰好满足了我这种“碎片化”学习的需求。它的篇幅不算特别厚重,而且“30天”这个概念也给了一个明确的学习目标。更重要的是,书中以非常务实的态度,从最基础的环节开始,一步步构建。我喜欢它那种“从小处着手,逐步完善”的思路,比如先实现一个简单的屏幕输出,再引入键盘输入,然后是中断处理等等。光盘提供的资源更是锦上添花,可以直接拿来运行和调试,这对于理解抽象的概念非常有帮助。虽然30天要“完全”掌握所有细节可能有些难度,但这本书绝对是一个非常好的入门和实践平台,让我能够快速对操作系统的核心有了一个清晰的认识。
评分说实话,当我拿到这本《30天自制操作系统》的时候,心里是抱着一丝怀疑的。毕竟“30天”这个时限听起来就很有挑战性,我担心它会为了赶进度而牺牲内容的深度。但事实证明,我的担忧是多余的。这本书的作者显然对如何引导读者学习有着深刻的理解。它并没有试图在一开始就构建一个功能齐全的复杂系统,而是从最核心、最关键的部分入手,逐步增加功能。这种“由简入繁”的教学方法,让我能够专注于理解每个阶段的核心概念。书中的案例设计也非常贴切,比如,在讲解中断处理时,作者会引导读者实现一个简单的键盘输入响应,这比单纯的理论讲解要生动有趣得多。我特别喜欢书中对一些常见误区的提醒和解释,这大大节省了我自己摸索的时间。光盘里的资源也非常丰富,包含了不同阶段的操作系统代码,方便我对比和学习。虽然30天完成一个完整的操作系统可能需要付出大量的努力和时间,但这本书提供了一个非常坚实的基础和清晰的路径,让我看到了实现这个目标的可能性。
评分这本书简直是为我量身定做的!我是一名在校的计算机专业学生,一直在寻找能够真正将理论知识付诸实践的项目。传统的操作系统课程虽然有讲解,但总感觉缺少了那种“亲手打造”的感觉。《30天自制操作系统》正是弥补了这一遗憾。它不仅仅是一本书,更像是一位经验丰富的导师,耐心地引导我一步步探索操作系统的世界。我尤其欣赏书中对每一项技术的讲解,比如如何编写汇编语言的引导程序,如何理解CPU的执行流程,以及如何实现简单的内存管理。这些内容虽然在其他教材中可能有所提及,但这本书将它们串联起来,形成了一个完整的学习链条。光盘中的源代码更是宝藏,我不仅可以运行它,还可以尝试修改和扩展,看看不同的设计会带来什么样的效果。通过这本书,我不仅巩固了课堂上学到的知识,还学到了很多书本上没有的实践技巧。我甚至开始思考,如何将书中的一些模块应用到我自己的课程设计项目中。
评分这本书绝对是技术学习者的一场及时雨!我一直对操作系统底层运作原理充满好奇,但市面上很多书籍要么过于理论化,要么需要扎实的计算机体系结构基础。这本《30天自制操作系统》的出现,恰恰填补了这个空白。它以一种非常直观、循序渐进的方式,带领读者一步步构建自己的操作系统。从最基础的 bootloader 到内核的初始化,再到简单的内存管理和任务调度,每一个章节都设计得恰到好处,不会让人望而却步。最让我惊喜的是,它提供了配套的光盘,里面包含了大量的源代码和调试工具,这对于动手实践来说至关重要。我花了整整一个周末,跟着书里的代码一点点调试,看着自己的代码在虚拟机里运行起来,那种成就感是难以言喻的。书中的讲解清晰易懂,即使是初学者也能理解其中的逻辑。而且,它并不是简单地复制代码,而是深入浅出地解释了每一行代码的意义和作用,这对于建立深层次的理解非常有帮助。总的来说,这本书是一次非常棒的学习体验,我强烈推荐给所有对操作系统感兴趣的朋友。
评分???????
评分不错,看着不错先买了,闲暇之时值得一读。
评分不错,有必要了解一下编程语言的执行过程
评分国内很少有日本人写的计算机书籍。买来看一下。
评分京东购物方便快捷 发货快
评分这书很好,能帮助你理解操作系统细节,我很满意,尤其京东送货速度就是飞快啊!!!!
评分。。。。。。。。
评分书质量不错,,,
评分看不懂,不过还是要认真看。
本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度,google,bing,sogou 等
© 2025 book.idnshop.cc All Rights Reserved. 静思书屋 版权所有