“一个操作系统的实现”最后一篇
终于,时隔这么多天,总算是把 第八章
完成了。从下篇开始,作者的写作风格大变,上篇基本上是把大部分的代码都弄出来讲,不过下篇的话,作者只拿了一些关键的代码,而且很多地方都做了修改,但是他却没有提到,所以,你只能自己去扒代码。在实现的过程中,出现了超级多的 bug
,debug
到要吐。不过,我不会像以前一样只会抱怨说:“这不科学!” 在这快一个星期的改错过程中,我学到了很多东西。不要总是抱怨改 bug
辛苦, bug
都是自己制造出来的,这个就只能怪自己了。最重要的一点,那就是,所有的 bug
都是思维的漏洞。所以,在你的思想体系还没有十分清晰的情况下,不要轻易写代码,不然,出现 bug
是很正常的事情。
好了,闲话不多说了。下面进入正题。
综述
这一章有几个比较大的改动。首先,上一章的 write 系统调用改成了 printx ,使之能够对 assert()
和 panic()
这两个函数进行特殊的处理,其他的功能基本一致。然后就是用 IPC
重新实现了 get_ticks() 这个函数。
IPC
这个是微内核的核心,它使得我们的系统系统调用可以大大减少。当初在看书的时候也确实花费了相当长的时间在理解这个东西。下面分别来说一下个中细节。
msg_send
首先,通讯通讯,肯定是有收有发。我们的进程通过 send_recv
这个函数发送消息,最终就是调用 msg_send
。首先判断是否死锁,之后再判断发送的目的地是否是接收消息的状态(即是否有 RECEIVING
标志),并且是否是接收来自当前进程的消息或者是任意进程的消息(就是那个 ANY),如果是,则将消息复制到接收端,清除接收端的 RECEIVING
标志,待接收信息指针指向空;否则,给当前的进程打上 SENDING
标志,待发送的消息赋给消息指针,待接收端赋给 p_sendto
(也就是将所有的信息保存在进程体里面。)接下来有做的是就是将当前进程加入到目标进程的消息发送队列了。这里有两种情况,躲过目标进程的消息接收队列为空,那么直接将当前的进程加入到目标进程的 q_sending
,否则的话,就要找到当前消息队列的最后一个(也就是找到那个 next_sending
为 0 的那个),然后把当前进程赋给队列最后一个的 next_sending
。然后,都要将消息队列的最后一个(也就是当前进程)的 next_sending
赋值为 0 。最后,调用 block()
函数,并最终调用了 schedule()
函数切换进程。注意,这里有一个很重要的点,就是它是如何实现进程切换的。我们知道,我们调用发送消息是通过系统中断来实现,在中断处理程序 sys_call
中的 save()
函数,会根据当前的 k_reenter
的值来分别将不同的东西压栈,而压入的内容,其实就是最终的进程切换程序 restart()
,并且最终在 sys_call
的最后一句 ret
来跳转到这个进程切换程序。 完整的消息发送过程就是这样。
msg_receive
接下来就是消息接收了。首先判断当前进程是否接收来自任意进程的消息,如果是,则判断当前接收消息的进程的消息队列是否有在等待发送的进程,如果有的话,就把消息队列的第一个赋给 p_sendto
变量,以便接下来的处理;如果不是接收任意进程的消息,而是有特定的发送对象,那么就检查那个特定对象是否在当前进程的消息发送队列中(也就是检查特定对象是否有 SENDING
标志位并且 p_sendto
指针是否指向当前进程),如果是,那么就遍历整个消息发送队列,如果是第一个,那最好,直接赋给 p_sendto
变量,否则,找到特定对象以及排在它前面的进程(这个排在前面的进程是有用的,后面会提到),分别赋给变量 p_sendto
以及 prev
。接下来就是真真正正的消息传递了,如果当前进程接收任意进程的消息或者接收特定进程的消息并且该进程在在消息发送队列的第一个,那么就将该进程从队列中提出来,并且将队列整体前移。否则,为了从夹在消息队列的中间的特定进程从队列中抽出来,并且不影响队列的整体,这时前面的那个 prev
就派上了用场,将 p_sendto
的 p_sendto
指针指向 prev
的 p_sendto
指针,这样就能够实现将其从队列中抽出来的目的了(如果你还是不懂的话,自己画个图就很容易懂了~)。然后就是一些收尾的工作,清除当前进程的 RECEIVING
标志,清空指向消息的指针。最后的最后,如果是最坏的情况,那就是既不接收任意进程的消息,同时特定进程不在消息队列中,那么,就将当前进程打上 RECEIVING
的标志位,将消息等各种信息保存在进程体中,然后阻塞进程。
全过程
好了,各种细节解释完了,接下来我们来看一个 IPC 的具体例子:就是用 IPC 来替换系统调用 get_ticks
,接下来,我将详细描述整个过程,以便大家能够有一个比较清晰的理解:
首先,由于 task_sys
系统进程,所以比用户进程先执行。整个进程其实就是一个守护进程,它是一个死循环,不断地接收信息,处理完之后再发送,然后继续如此。首先它先接收任意进程的消息,但是很明显,当前还没有进程给它发送消息,所以 task_sys
被阻塞了。然后进程切换到用户进程, 用户进程调用函数 get_ticks()
, get_ticks()
函数初始化一个消息变量,并且将消息类型设置为 GET_TICKS
,然后调用一个封装函数 send_recv()
,并且最终将调用系统调用 sys_sendrecv
。因为是 BOTH
类型的,所以是先发送,然后在接送。首先是发送消息,由于上面的 task_sys
已经执行,也就是说,接收端表明它在接收一个函数,将消息复制给 task_sys
的消息指针,然后消除 task_sys
的 RECEIVING
标志位,注意,因为此时 task_sys
的 RECEIVING
标志位已经被消除了,所以 task_sys
已经被解除阻塞了,只是进程切换还没有运行而已 。至此,消息发送成功。然后,即使等待接收,很明显,虽然 task_sys
已经被解除阻塞了,但是进程切换还没有运行,所以,消息还没哟被处理,所以,用户进程被阻塞了。接下来,进程切换,** task_sys
从上次被阻塞的地方继续运行(这一点很重要,我一开始就是忽略了这一点,导致了一直无法正确理解)**。因为已经成功地接收了消息,接下来,将 ticks
赋给消息,然后发送消息给用户进程,上面说道,用户进程此时是 RECEIVING
的状态,所以,将消息复制给用户进程,然后消除 RECEIVING
的标志位,跟上面说的一样,此时用户进程已经被解除阻塞了,只是进程切换还没有运行而已。消息发送成功。然后,进程切换,切换到用户进程,从上次被阻塞的地方继续。最后用户进程的 send_recv
成功执行,get_ticks
将消息中的 ticks
返回,至此,一个消息传递的全过程完整结束了!
简单来说,整个过程其实就是这样:
task_sys
等待接收消息并且被阻塞 -> 用户进程调用 get_ticks()
并且最终调用 msg_send()
还有 msg_receive()
-> 先是 msg_send()
,将消息复制给 task_sys
,消息发送成功 -> 然后是 msg_receive()
,用户进程等待接收消息并且在该处被阻塞 -> task_sys
解除阻塞,将消息处理完之后发送给用户进程,用户进程被解除阻塞 -> 用户进程继续从被阻塞的地方运行,并且最终成功返回。
写得有点多,不过这个 IPC
机制真的很重要!
遇到的 BUG
一开始一运行就老是报 #PAGE FAULT
的错误,不知道是为什么?最后终于发现了 BUG
出现在 ./kernel/main.c
文件上,总共有两个疏忽的点:
- 就是在判断是是用户进程还是好任务的时候,如果是用户进程,一开始我是这样写的:
p_proc = user_proc_table + i;
这其实是不对的,得这样:
p_proc = user_proc_table + i - NR_TASKS;
这样就好了。
- 就是在给每一个进程赋初始的
TTY
时,也出现了像上面一样的疏忽:
proc_table[NR_TASKS + 0] = 0;
而我就是忘记了把那个 NR_TASKS
加上去了。。。
总之,都是疏忽,都是思维漏洞!!!
好了,第八章总算是告一段落了,接下来就要向我最期待的第九章 – 硬盘进军了!
EOF
Author: simowce
Permalink: https://blog.simowce.com/chapter-8-IPC/
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。