读过linux内核代码的朋友可能都有一个共同的感受:那就是错综复杂,环环相扣。诚然,计算机操作系统是一个复杂的系统,虽然并不是最复杂的(航空母舰的系统要复杂得多),但是各个部分的相互影响往往需要设计者拥有更加缜密的构思和更加全面的考虑。
 
航空母舰的系统的各个部分可以看作是隔离的——厕所部分的设计者不需要了解雷达部分的事情——这是显而易见的。但是这种思想若被带进操作系统的设计,就意味着致命的错误——因为计算机的操作系统是浑然一体的(想起在小学学习“浑然一体”这个词时,老师解释说这个词是完整的不可分割的一个整体的意思。当时不甚理解,如今读完了内核代码后才恍然大悟——再没有一个词能比它更适合来形容操作系统了)。例如文件系统的错误会导致内存中的错误,并且进而使整个系统陷入崩溃的险境。
 
正因为如此,操作系统的设计者要时时兼顾全局。这就使得内核代码有一些很小的细节,看似信手拈来,可有可无,实则意义重大,往往是代码中的关键。能否看透这些代码中的玄机,可以说是是否理解内核的重要标准。今后几篇文章我们就来说说几个比较微妙的小机。
 
bread()中的加锁问题 
 
在buffer.c中第267行有一个bread()函数,它的作用是从指定设备上读取内容到缓冲区中,并且返回缓冲区的指针。注意,这里的缓冲区大小为一页(4KB),是由操作系统从缓冲区队列中找到空闲的,然后动态分配的。这就有了一个问题——若从队列获得了缓冲区后,立即发生了中断(或者是异常什么的),当从中断处理程序返回,再次执行时,分配好的缓冲区已被别的进程占用并且加了锁(参考211--230行的代码),那该怎么办呢??这里我借用一下毛先生的情景分析发来说明一下。在我们这个情景中会出现这样的代码调用顺序:bread()-->getblk()-->wait_on_buffer()。
 
wait_on_buffer()函数代码如下:
static inline void wait_on_buffer(struct buffer_head * bh)
{
 cli();
 while(bh->b_lock)
  sleep(&bh->b_wait);
 sti();
}
进入wait_on_buffer()执行后,首先会关中断,接着检测到指定的缓冲区已加锁,然后休眠,直到拥有锁的进程释放锁后,调用wake_up()将其唤醒。然后才开中断。为什么要关中断呢??相信学过操作系统原理的朋友都知道,缓冲区属于公共资源,对公共资源的分配必定会引起进程之间竞争,这是多道程序设计所必须面对的问题。因此所有对公共资源的操作都应该互斥,也就是说应该在临界区里完成。这样的话,这一对儿cli()和sti()的作用就很明显了。关闭中断,可以保证操作“原子”的完成,也就实现了互斥。
 
这时细心的朋友要问了,一个进程为了等待解锁而关闭中断休眠,并且只有当它被唤醒时才会开中断,这样不会导致内核的死锁吗??要知道,关闭了中断就意味着不在有时钟中断,也就没有了调度程序,那么一个进程运行结束后,cpu将不再把任何就绪的程序加载运行,系统陷入死机状态。这是怎么回事??是内核的漏洞吗??
 
回答是否定的。我要说的是:虽然没有了时钟中断,但是并不意味着没有了调度。sleep()函数中在置进程状态task_interuptable之后,就会做一次调度,选择下一个就绪的进程运行。时钟中断处理函数中会发生调度,但并不是说调度必须和时钟中断“绑定”。这个概念一定要搞清楚。
 
现在假设有A1,A2...An个进程等待缓冲区解锁,一个进程B拥有锁(因为互斥的关系,任意时间只能有一个进程有锁)。A1进程关中断,至此不再有时钟中断检测到缓冲区加锁,调度,休眠。A1不再被选中,只有A2--An和B可以运行。同理,A2--An也因为同样原因休眠。这时只有B进程为就绪态,因此它理所应当地独占cpu。当它对缓冲区操作结束后,就会调用wake_up()唤醒所有在等待队列中的进程(A1--An)。现在仅仅是唤醒进程,中断没有了,什么时候调度呢??
 
朋友们可以仔细看看,后面不再有显式调用调度程序的时候。系统调用exit()结尾调用了调度程序,可是通读代码,并没有哪个操作将它加到每个进程的末尾啊?进程执行完后也不见它跳到exit()系统调用处?看上去好象进入了死胡同,其实不然。这个问题在内核中并没有给出答案,要解决它还得从编译起入手。
 
以gcc为例,每个编译系统在编译结束后,都要在程序代码的末尾加上一句“exit();”。这里的exit()是c的库函数,为调用系统调用提供了一个接口。了解了这一点后,后面的过程就不难理解了。
 
拥有锁的进程唤醒等待的n个进程后,在exit()中调用了调度程序。因为此进程已经退出,所以其状态为“僵死”,不在参与调度。被唤醒的n个进程会有一个获得锁,同时另外n-1个进程会再次检测到缓冲区加锁而进入休眠状态。如此循环下去直到最后一个等待解锁的进程成功运行,退出临界区的操作后会做一次sti();。这时,久违了的中断才能再次恢复,系统也恢复到常态。
 
好了,到此我们的问题就解决了。各位是不是觉得这段代码很精妙呢?一对开关中断的操作竟有这么多说道,姑且抛开方法的好坏不提,单就代码而言确实是够强的了。我们不得不佩服linus的巧妙构思,想想自己写个几百行的小程序还漏洞百出,真是惭愧啊。
 
这种处理方法的缺点是明显的。为了处理等待队列,需要长时间的关闭中断,这样会导致一些重要的中断信号的流失。
 
例如,进程A发出读硬盘的操作请求,再等待硬盘就绪信号时出现了咱们所说的这个情景。那么显而意见,这个信号将因为关中断而被忽略;进程A永远也无法得到硬盘就绪的通知,其操作也就永远无法完成了。
 
解决这个问题的一个方法是使用可以记录一定历史时期到达的信号的中断接受器。这需要特别的硬件支持。另一个方法是使“bottom-half”。linux 2.4.0的内核就采用这种方法,当然这是后话。
 
最后我想说一下我的一个同学问我的问题:他认wait_on_buffer()中关中断是为了防止这种情况——A进程带所,且需要B进程所占有的资源,若A带着锁中断,这时B来检测锁后休眠,那么A和B都会永远是等待态,死锁出现。因此关中断是为了避免中断干扰。其实他所说的情况不会出现。道理很简单,那就是在内核态发生中断后不会发生调度。因此也不会出现A进程未执行完就执行B进程的情况。
 
以上就是我自己的一点儿浅薄的意见,有不对的地方,欢迎各位指正。本人的特点是“闻过,则喜”。同时,我也诚心希望和高手们交流、切磋,提高自己的水平。有朋自远方来,不亦乐乎?

此帖转自:[url]http://www.programfan.com/team/team.asp?team_id=780[/url]