荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: Ewi.bbs@siyu.dhs.org (Ewi), 信区: Linux
标  题: Linux内核注释——第10章 对称多处理(SMP)
发信站: 思雨小语 (Sun May 25 18:52:35 2003)
转信站: SZU!news.tiaozhan.com!Siyu


在全书的讨论过程中,我一直在忽略SMP代码,而倾向于把注意力集中在只涉及一
个处理器的相对简单的情况。现在已经到了重新访问读者已经熟悉的一些内容的时
候了,不过要从一个新的角度来审视它:当内核必须支持多于一个CPU的机器时将
发生什么?
在一般情况下,使用多于一个CPU来完成工作被称为并行处理(parallel
processing),它可以被想象成是一段频谱范围,分布式计算(distributed
computing)在其中一端,而对称多处理(SMP—symmetric multiprocessing)在
另一端。通常,当你沿着该频谱从分布式计算向SMP移动时,系统将变得更加紧密
耦合——在CPU之间共享更多的资源——而且更加均匀。在一个典型的分布式系统
中,每个CPU通常都至少拥有它自己的高速缓存和RAM。每个CPU还往往拥有自己的
磁盘、图形子系统、声卡,监视器等等。
在极端的情形下,分布式系统经常不外乎就是一组普通的计算机,虽然它们可能具
有完全不同的体系结构,但是都共同工作在某个网络之上——它们甚至不需要在同
一个LAN里。读者可能知道的一些有趣的分布式系统包括:Beowulf,它是对相当传
统而又极其强大的分布式系统的一个通用术语称谓;SETI@home,它通过利用上百
万台计算机来协助搜寻地外生命的证据,以及distributed.net,它是类似想法的
另一个实现,它主要关注于地球上产生的密码的破解。
SMP是并行处理的一个特殊情况,系统里所有CPU 都是相同的。举例来说,SMP就是
你共同支配两块80486或两块Pentium(具有相同的时钟速率)处理器,而不是一块
80486和一块Pentium,或者一块Pentium和一块PowerPC。在通常的用法中,SMP也
意味着所有CPU都是“在相同处境下的”——那就是它们都在同一个计算机里,通
过特殊用途的硬件进行彼此通信。
SMP系统通常是另一种平常的单一(single)计算机——只不过具有两个或更多的
CPU。因此,SMP系统除了CPU以外每样东西只有一个——一块图形卡、一个声音卡
,等等之类。诸如RAM和磁盘这样以及类似的资源都是为系统的CPU们所共享的。(
尽管现在SMP系统中每个CPU都拥有自己的高存缓存的情况已经变得愈发普遍了。)

分布式配置需要很少的或者甚至不需要来自内核的特殊支持;节点之间的协同是依
靠用户空间的应用程序或者诸如网络子系统之类未经修改的内核组件来处理的。但
是SMP在计算机系统内创建了一个不同的硬件配置,并由此需要特殊用途的内核支
持。比如,内核必须确保CPU在访问它们的共享资源时要相互合作——这是一个读
者在UP世界中所不曾遇到的问题。
SMP的逐渐普及主要是因为通过SMP所获得的性能的提高要比购买几台独立的机器再
把它们组合在一起更加便宜和简单,而且还因为它与等待下一代CPU面世相比要快
的多。
非对称多CPU的配置没有受到广泛支持,这是因为对称配置情况所需的硬件和软件
支持通常较为简单。不过,内核代码中平台无关的部分实际上并不特别关心是否
CPU是相同的——即,是否配置是真正对称的——尽管它也没有进行任何特殊处理
以支持非对称配置。例如,在非对称多处理系统中,调度程序应该更愿意在较快的
而不是较慢的CPU上运行进程,但是Linux内核没有对此进行区别。
谚语说得好,“天下没有白吃的午餐”。对于SMP,为提高的性能所付出的代价就
是内核复杂度的增加和协同开销的增加。CPU必须安排不互相干涉彼此的工作,但
是它们又不能在这种协同上花费太多时间以至于它们显著地耗费额外的CPU能力。

代码的SMP特定部分由于UP机器存在的缘故而被单独编译,所以仅仅因为有了SMP寄
存器是不会使UP寄存器慢下来的。这满足两条久经考验的原理:“为普遍情况进行
优化”(UP机器远比SMP机器普遍的多)以及“不为用不着的东西花钱”。
并行程序设计概念及其原语
 具有两个CPU的SMP配置可能是最简单的并行配置,但就算是这最简单的配置也揭
开了未知问题的新领域——即使要两块相同的CPU在一起协调的工作,时常也都像
赶着猫去放牧一样困难。幸运的是,至少30年前以来,就在这个项目上作了大量和
非常熟悉的研究工作。(考虑到第一台电子数字计算机也只是在50年前建造的,那
这就是一段令人惊讶的相当长的时间了。)在分析对SMP的支持是如何影响内核代
码之前,对该支持所基于的若干理论性概念进行一番浏览将能够极大的简化这个问
题。
注意:并非所有这些信息都是针对SMP内核的。一些要讨论的问题甚至是由UP内核
上的并行程序设计所引起的,既要支持中断也要处理进程之间的交互。因此即使你
对SMP问题没有特别的兴趣,这部分的讨论也值得一看。
原子操作
在一个并行的环境里,某些动作必须以一种基本的原子方式(atomically)执行—
—即不可中断。这种操作必须是不可分割的,就象是原子曾经被认为的那样。
作为一个例子,考虑一下引用计数。如果你想要释放你所控制的一份共享资源并要
了解是否还有其它(进程)仍在使用它,你就会减少对该共享资源的计数值并把该
值与0进行对照测试。一个典型的动作顺序可能如下开始:
1.  CPU把当前计数值(假设是2)装载进它的一个寄存器里。
2.  CPU在它的寄存器里把这个值递减;现在它是1。
3.  CPU把新值(1)写回内存里。
4.  CPU推断出:因为该值是1,某个其它进程仍在使用着共享对象,所以它将不会
释放该对象。
对于UP,应不必在此考虑过多(除了某些情况)。但是对于SMP就是另一番景象了
:如果另一个CPU碰巧同时也在作同样的事情应如何处理呢?最坏的情形可能是这
样的:
1.  CPU A把当前计数值(2)装载进它的一个寄存器里。
2.  CPU B把当前计数值(2)装载进它的一个寄存器里。
3.  CPU A在它的寄存器里把这个值递减;现在它是1。
4.  CPU B在它的寄存器里把这个值递减;现在它是1。
5.  CPU A把新值(1)写回内存里。
6.  CPU B把新值(1)写回内存里。
7.  CPU A推断出:因为该值是1,某个其它进程仍在使用着共享对象,所以它将不
会释放该对象。
8.  CPU B推断出:因为该值是1,某个其它进程仍在使用着共享对象,所以它将不
会释放该对象。
内存里的引用计数值现在应该是0,然而它却是1。两个进程都去掉了它们对该共享
对象的引用,但是没有一个能够释放它。
这是一个有趣的失败,因为每个CPU都作了它所应该做的事情,尽管这样错误的结
果还是发生了。当然这个问题就在于CPU没有协调它们的动作行为——右手不知道
左手正在干什么。
你会怎样试图在软件中解决这个问题呢?从任何一个CPU的观点来看待它——比如
说是CPU A。需要通知CPU B它不应使用引用计数值,由于你想要递减该值,所以不
管怎样你最好改变某些CPU B所能见到的信息——也就是更新共享内存位置。举例
来说,你可以为此目的而开辟出某个内存位置,并且对此达成一致:若任何一个
CPU正试图减少引用计数它就包含一个1,如果不是它就为0。使用方法如下:
1.  CPU A从特殊内存位置出取出该值把它装载进它的一个寄存器里。
2.  CPU A检查它的寄存器里的值并发现它是0(如果不是,它再次尝试,重复直到
该寄存器为0为止。)
3.  CPU A把一个1写回特殊内存位置。
4.  CPU A访问受保护的引用计数值。
5.  CPU A把一个0写回特殊内存位置。
糟糕,令人不安的熟悉情况又出现了。以下所发生的问题仍然无法避免:
1.  CPU A从特殊内存位置出取出该值把它装载进它的一个寄存器里。
2.  CPU B从特殊内存位置出取出该值把它装载进它的一个寄存器里。
3.  CPU A检查它的寄存器里的值并发现它是0。
4.  CPU B检查它的寄存器里的值并发现它是0。
5.  CPU A把一个1写回特殊内存位置。
6.  CPU B把一个1写回特殊内存位置。
7.  CPU A访问受保护的引用计数值。
8.  CPU B访问受保护的引用计数值。
9.  CPU A把一个0写回特殊内存位置。
10. CPU B把一个0写回特殊内存位置。
好吧,或许可以再使用一个特殊内存位置来保护被期望保护初始内存位置的那个特
殊内存位置……。
面对这一点吧:我们在劫难逃。这种方案只会使问题向后再退一层,而不可能解决
它。最后,原子性不可能由软件单独保证——必须要有硬件的特殊帮助。
在x86平台上,lock指令正好能够提供这种帮助。(准确地说,lock是一个前缀而
非一个单独的指令,不过这种区别和我们的目的没有利害关系。)lock指令用于在
随后的指令执行期间锁住内存总线——至少是对目的内存地址。因为x86可以在内
存里直接减值,而无需明确的先把它读入一个寄存器中,这样对于执行一个减值原
子操作来说就是万事俱备了:lock内存总线然后立刻对该内存位置执行decl操作。

函数atomic_dec(10241行)正好为x86平台完成这样的工作。LOCK宏的SMP版本在
第10192行定义并扩展成lock指令。(在随后的两行定义的UP版本完全就是空的—
—单CPU不需要保护自己以防其它CPU的干扰,所以锁住内存总线将完全是在浪费时
间。)通过把LOCK宏放在内嵌编译指令的前边,随后的指令就会为SMP内核而被锁
定。如果CPU B在CPU A发挥作用时执行了atomic_dec函数,那么CPU B就会自动的
等待CPU A把锁移开。这样就能够成功了!
这样还只能说是差不多。最初的问题仍然没有被很好的解决。目标不仅是要自动递
减引用计数值,而且还要知道结果值是否是0。现在可以完成原子递减了,可是如
果另一个处理器在递减和结果测试之间又“偷偷的”进行了干预,那又怎么办呢?

幸运的是,解决这个部分问题不需要来自CPU的特殊目的的帮助。不管加锁还是未
锁,x86的decl指令总是会在结果为0时设置CPU的Zero标志位,而且这个标志位是
CPU私有的,所以其它CPU的所为是不可能在递减步骤和测试步骤之间影响到这个标
志位的。相应的,atomic_dec_and_test(10249行)如前完成一次加锁的递减,接
着依据CPU的Zero标志位来设置本地变量c。如果递减之后结果是0函数就返回非零
值(真)。
如同其它定义在一个文件里的函数一样,atomic_dec和atomic_dec_and_test都对
一个类型为atomic_t的(10205行)对象进行操作。就像LOCK,atomic_t对于UP和
SMP也有不同的定义方式——不同之处在于SMP情况里引入了volatile限定词,它指
示gcc不要对被标记的变量做某种假定(比如,不要假定它可以被安全的保存在一
个寄存器里)。
顺便提及一下,读者在这段代码里看到的垃圾代码__atomic_fool_gcc据报告已不
再需要了;它曾用于纠正在gcc的早期版本下代码生成里的一个故障。
Test-And-Set
经典的并行原语是test-and-set。test-and-set操作自动地从一个内存位置读取一
个值然后写入一个新值,并把旧值返回。典型的,该位置可以保存0或者1,而且
test-and-set所写的新值是1——因此是“设置(set)”。与test-and-set对等的
是test-and-clear,它是同样的操作除了写入的是0而不是1。一些test-and-set的
变体既能写入1也可以写入0,因此test-and-set和test-and-clear就能够成为一体
,只是操作数不同而已。
test-and-set原语足以实现任何其它并行安全的操作。(实际上,在某些CPU上
test-and-set是唯一被提供的此类原语。)比如,原本test-and-set是能够用于前
边的例子之中来保护引用计数值的。相似的方法以被尝试——从一个内存位置读取
一个值,检查它是否为0,如果是则写入一个1,然后继续访问受保护的值。这种尝
试的失败并不是因为它在逻辑上是不健全的,而是因为没有可行的方法使其自动完
成。假使有了一个原子的test-and-set,你就可以不通过使用lock来原子化decl的
方法而顺利通过了。
然而,test-and-set也有缺点:
?   它是一个低级的原语——在所有与它打交道时,其它原语都必须在它之上被执行

?   它并不经济——当机器测试该值并发现它已经是1了怎么办呢?这个值在内存里
不会被搞乱,因为只要用同样的值复写它即可。可事实是它已被设置就意味着其它
进程正在访问受保护的对象,所以还不能这样执行。额外需要的逻辑——测试并循
环——会浪费CPU时钟周期并使得程序变得更大一些(它还会浪费高速缓存里的空
间)。
x86的lock指令使高级指令更容易执行,但是你也可以在上执行原子test-and-set
操作。最直接的方式是把lock和btsl指令(位test-and-set)联合起来使用。这种
方法要被本章后边介绍的自旋锁(spinlock)所用到。
另一种在x86上实现的方法是用它的xchg(exchange)指令,它能够被x86自动处理
,就好像它的前面有一个lock指令一样——只要它的一个操作数是在内存里。
xchg要比lock/  btsl组合更为普遍,因为它可以一次交换8、16,或者32位而不仅
仅是1位。除了一个在arch/i386/kernel/entry.S里的使用之外,内核对xchg指令
的使用都隐藏在xchg宏(13052行)之后,而它又是在函数__xchg(13061行)之上
实现的。这样是便于在平台相关的代码里内核代码也可以使用xchg宏;每种平台都
提供它自己对于该宏的等价的实现。
有趣的时,xchg宏是另一个宏,tas(test-and-set——13054行)的基础。然而,
内核代码的任何一个地方都没有用到这个宏。
内核有时候使用xchg宏来完成简单的test-and-set操作(尽管不必在锁变得可用之
前一直循环,如同第22770行),并把它用于其它目的(如同第27427行)。
信号量
第9章中讨论了信号量的基本概念并演示了它们在进程间通信中的用法。内核为达
自己的目的有其特有的信号量实现,它们被特别的称为是“内核信号量”。(在这
一章里,未经修饰的名词“信号量”应被理解为是“内核信号量”。)第9章里所
讨论的基本信号量的概念同样适用于内核信号量:允许一个可访问某资源用户的最
大数目(最初悬挂在吊钩上钥匙的特定数目),然后规定每个申请资源者都必须先
获得一把钥匙才能使用该资源。
到目前为止,你大概应该已经发现信号量如何能够被建立在test-and-set之上并成
为二元(“唯一钥匙”)信号量,或者在像atomic_dec_and_test这样的函数之上
成为计数信号量的过程。内核正好就完成着这样的工作:它用整数代表信号量并使
用函数down(11644行)和up(11714行)以及其它一些函数来递减和递增该整数。
读者将看到,用于减少和增加整数的底层代码和atomic_dec_and_test及其它类似
函数所使用的代码是一样的。
作为相关历史事件的提示,第一位规范信号量概念的研究者,Edsger Dijistra是
荷兰人,所以信号量的基础操作就用荷兰语命名为:Proberen和Verhogen,常缩写
成P和V。这对术语被翻译成“测试(test)”(检查是否还有一把钥匙可用,若是
就取走)和“递增(increment)”(把一个钥匙放回到吊钩之上)。那些词首字
母正是在前一章中所引入的术语“获得(procure)”和“交出(vacate)”的来
源。Linux内核打破了这个传统,用操作down和up的称呼取代了它们。
内核用一个非常简单的类型来代表信号量:定义在11609行的struct semaphore。
他只有三个成员:
?   count——跟踪仍然可用的钥匙数目。如果是0,钥匙就被取完了;如果是负数,
钥匙被取完而且还有其它申请者在等待它。另外,如果count是0或负数,那么其它
申请者的数目就等于count的绝对值。
Sema_init宏(11637行)允许count被初始化为任何值,所以内核信号量可以是二
元的(初始化count为1)也可以是计数型的(赋予它某个更大的初始值)。所有内
核信号量代码都完全支持二元和计数型信号量,前者可作为后者的一个特例。不过
在实践中count总是被初始化为1,这样内核信号量也总是二元类型的。尽管如此,
没有什么能够阻止一个开发者将来增加一个新的计数信号量。
要顺便提及的是,把count初始化为正值而且用递减它来表明你需要一个信号量的
方法并没有什么神秘之处。你也可以用一个负值(或者是0)来初始化计数值然后
增加它,或者遵循其它的方案。使用正的数字只是内核所采用的办法,而这碰巧和
我们头脑中的吊钩上的钥匙模型吻合得相当好。的确,正如你将看到的那样,内核
锁采用的是另一种方式工作——它被初始化为负值,并在进程需要它时进行增加。

?   waking——在up操作期间及之后被暂时使用;如果up正在释放信号量则它被设置
为1,否则是0。
?   wait——因为要等待这个信号量再次变为可用而不得不被挂起的进程队列。
down
11644:down操作递减信号量计数值。你可能会认为它与概念里的实现一样简单,
不过实际上远不是这样简单。
11648:减少信号量计数值——要确保对SMP这是自动完成的。对于SMP来说(当然
也适于UP),除了被访问的整数是在一个不同类型的struct之内以外,这同在
atomic_dec_and_test中所完成的工作本质上是相同的。
    读者可能会怀疑count是否会下溢。它不会:进程总是在递减count之后进入休眠
,所以一个给定的进程一次只能获得一个信号量,而且int具有的负值要比进程的
数目多的多。
11652:如果符号位被设置,信号量就是负值。这意味着甚至它在被递减之前就是
0或者负值了,这样进程无法得到该信号量并因此而应该休眠一直到它变成可用。
接下来的几行代码十分巧妙地完成了这一点。如果符号位被设置则执行js跳转(即
若decl的结果是负的它就跳转),2f标识出跳转的目的地。2f并非十六进制值——
它是特殊的GNU汇编程序语法:2表示跳转到本地符号“2”,f表示向前搜索这个符
号。(2b将表示向后搜索最近的本地符号“2”。)这个本地符号在第11655行。
11653:分支转移没有执行,所以进程得到了信号量。虽然看起来不是这样,但是
这实际已经到达down的末尾。稍后将对此进行解释。
11654:down的技巧在于指令.section紧跟在跳转目标的前面,它表示把随后的代
码汇编到内核的一个单独的段中——该段被称为.text.lock。这个段将在内存中被
分配并标识为可执行的。这一点是由跟在段名之后的ax标志字符串来指定的——注
意这个ax与x86的AX寄存器无关。
    这样的结果是,汇编程序将把11655和11656行的指令从down所在的段里转移到可
执行内核的一个不同的段里。所以这些行生成的目标代码与其前边的行所生成的代
码从物理上不是连续的。这就是为什么说11653行是down的结尾的原因。
11655:当信号量无法得到时跳转到的这一目的行。Pushl $1b并不是要把十六进制
值1b压入栈中——如果要执行那种工作应该使用pushl $0x1b(也可以写成是不带
$的)。正确的解释是,这个1b和前边见到的2f一样都是GNU汇编程序语法——它指
向一个指令的地址;在此情形中,它是向后搜索时碰到的第一个本地标识“1”的
地址。所以,这条指令是把11653行代码的地址压入栈中;这个地址将成为返回地
址,以便在随后的跳转操作之后,执行过程还能返回到down的末尾。
11656:开始跳转到__down_failed(不包括在本书之内)。这个函数在栈里保存几
个寄存器并调用后边要介绍的__down(26932行)来完成等待信号量的工作。一旦
__down返回了,__down_failed就返回到down,而它也随之返回。一直到进程获得
了信号量__down才会返回;最终结果就是只要down返回,进程就得到信号量了,而
不管它是立刻还是经过等待后获得的它。
11657:伪汇编程序指令.previous的作用未在正式文档中说明,但是它的意思肯定
是还原到以前的段中,结束11654行里的伪指令.section的作用效果。
down_interruptible
11664:down_interruptible函数被用于进程想要获得信号量但也愿意在等待它时
被信号中断的情况。这个函数与down的实现非常相似,不过有两个区别将在随后的
两段里进行解释。
11666:第一个区别是down_interruptible函数返回一个int值来指示是否它获得了
信号量或者被一个信号所打断。在前一种情况里返回值(在result里)是0,在后
一种情况里它是负值。这部分上是由11675行代码完成的,如果函数未经等待获得
了信号量则该行把result设置为0。
11679:第二个区别是down_interruptible函数跳转到
__down_failed_interruptible(不包括在本书之内)而不是__down_failed。因循
__down_failed建立起来的模式,__down _failed_interruptible只是调整几个寄
存器并调用将在随后进行研究的__down_interruptible函数(26942行)。要注意
的是11676行为__down_failed_ interruptible设置的返回目标跟在xorl之后,
xorl用于在信号量可以被立刻获得的情况中把result归0。down_interruptible函
数的返回值再被复制进result中。
down_trylock
11687:除了调用__down_failed_trylock函数(当然还要调用26961行的
__down_trylock函数,我们将在后面对它进行检查)之外,down_trylock函数和
down_interruptible函数相同。因此,在这里不必对down_trylock函数进行更多解
释。
DOWN_VAR
26900:这是作为__down和_down_interruptible共同代码因子的三个宏中的第一个
。它只是声明了几个变量。
DOWN_HEAD
26904:这个宏使任务tsk(被DOWN_VAR所声明)转移到task_state给出的状态,然
后把tsk添加到等待信号量的任务队列。最后,它开始一个无限循环,在此循环期
间当__down和__down_interruptible准备退出时将使用break语句结束该循环。
DOWN_TAIL
26926:这个宏完成循环收尾工作,把tsk设置回task_state的状态,为再次尝试获
得信号量做准备。
26929:循环已经退出;tsk已或者得到了信号量或者被一个信号中断了(仅适于
__down_ interruptible)。无论哪一种方式,任务已准备再次运行而不再等待该
信号量了,因此它被转移回TASK_RUNNING并从信号量的等待队列里被注销。
__down
26932:__down和__down_interruptible遵循以下模式:
1.  用DOWN_VAR声明所需的本地变量,随后可能还有补充的本地变量声明。
2.  以DOWN_HEAD开始进入无穷循环。
3.  在循环体内完成函数特定的(function-specific)工作。
4.  重新调度。
5.  以DOWN_TAIL结束。注意对schedule的调用(26686行,在第7章里讨论过)可以
被移进DOWN_TAIL宏中。
6.  完成任何函数特定的收尾工作。
我将只对函数特定的步骤(第3和第6步)进行讨论。
26936:__down的循环体调用waking_non_zero(未包括),它自动检查
sem->waking来判断是否进程正被up唤醒。如果是这样,它将waking归零并返回1(
这仍然是同一个原子操作的一部分);如果不是,它返回0。因此,它返回的值指
示了是否进程获得了信号量。如果它获得了值,循环就退出,接着函数也将返回。
否则,进程将继续等待。
    顺便要说明的是,观察一下__down尝试获得信号量是在调用schedule之前。如果
信号量的计数值已知为负值时,为什么不用另一种相反的方式来实现它呢?实际上
它对于第一遍循环之后的任何一遍重复都是没有影响的,但是去掉一次没有必要的
检查可以稍微加快第一遍循环的速度。如果需要为此提出什么特别的理由的话,那
可能就是因为自从信号量第一次被检查之后的几个微秒内它就应该可以被释放(可
能是在另一个处理器上),而且额外获取标志要比一次额外调度所付出的代价少得
多。因此__down可能还可以在重新调度之前做一次快速检查。
__down_interruptible
26942:__down_interruptible除了允许被信号中断以外,它和__down在本质上是
一样的。
26948:所以,当获取信号量时对waking_non_zero_interruptible(未包括)进行
调用。如果它没能得到信号量就返回0,如果得到就返回1,或者如果它被一个信号
所中断就返回–EINTR。在第一种情况下,循环继续。
26958:否则,__down_interruptible退出,如果它得到信号量就返回0(不是1)
,或者假如被中断则返回–EINTR。
__down_trylock
26961:有时在不能立刻获得信号量的情况下,内核也需要继续运行。所以,
__down_trylock不在循环之内。它仅仅调用waking_nonzero_trylock(未包括),
该函数夺取信号量,如果失败就递增该信号量的count(因为内核不打算继续等待
下去)然后返回。
up
11714:我们已经详尽的分析了内核尝试获得信号量时的情况,也讨论了它失败时
的情况。现在是考察另一面的时候了:当释放一个信号量时将发生什么。这一部分
相对简单。
11721:原子性地递增信号量的计数值。
11722:如果结果小于等于0,就有某个进程正在等待被唤醒。up向前跳转到11725
行。
11724:up采用了down里同样的技巧:这一行进入了内核的单独的一段,而不是在
up本身的段内。up的末尾的地址被压入栈然后up跳转到__up_wakeup(未包括)。
这里完成如同__down_failed一样的寄存器操作并调用下边要讨论的__up函数。
__up
26877:__up函数负责唤醒所有等待该信号量的进程。
26897:调用wake_one_more(未包括在本书中),该函数检查是否有进程在等待该
信号量,如果有,就增加waking成员来通知它们可以尝试获取它了。
26880:利用wake_up宏(16612行),它只是调用__wake_up函数(26829行)来唤
醒所有等待进程。
__wake_up
26829:正如在第2章中所讨论的那样,__wake_up函数唤醒所有传递给它的在等待
队列上的进程,假如它们处于被mode所隐含的状态之一的话。当从wake_up被调用
时,函数唤醒所有处于TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE状态的进程;
当从wake_up_interruptible(16614行)被调用时,它只唤醒处于
TASK_INTERRUPTIBLE状态的任务。
26842:进程用wake_up_process(26356行)被唤醒,该函数曾在以前提到过,它
将在本章随后进行详细介绍。
现在所感兴趣的是唤醒所有进程后的结果。因为__wake_up唤醒所有队列里的进程
,而不仅仅是队列里的第一个,所以它们都要竞争信号量——在SMP里,它们可以
精确的同时做这件事。通常,获胜者将首先获得CPU。这个进程将是拥有最大“
goodness”的进程(回忆一下第7章中26338行对goodness的讨论)。 这一点意义
非常重大,因为拥有更高优先权的进程应该首先被给予继续其工作的机会。(这对
于实时进程尤其重要。)
这种方案的不足之处是有发生饥饿(starvation)的危险,这发生在一个进程永远
不能得到它赖以继续运行的资源时。这里可能会发生饥饿现象:假如两个进程反复
竞争同一个信号量,而第一个进程总是有比第二个更高的优先权,那么第二个进程
将永远不会得到CPU。这种场景同它应该的运行方式存在一定差距——设想一个是
实时进程而另一个以20的niceness运行。我们可以通过只唤醒队列里第一个进程的
方法来避免这种饥饿的危险,可是那样又将意味着有时候会耽误从各个方面来说都
更有资格的进程对CPU的使用。
以前对此没有讨论过,可是Linux的调度程序在适当的环境下也能够使得CPU的一个
进程被彻底饿死。这不完全是一件坏事——只是一种设计决策而已——而且至少应
用于通篇内核代码的原则是一致的,这就很好。还要注意的是使用前边讨论过的其
它机制,饥饿现象也同样会发生。例如说,test-and-set原语就是和内核信号量一
样的潜在饥饿根源。
无论如何,在实际中,饥饿是非常少见的——它只是一个有趣的理论案例。
Spinlocks
这一章里最后一个重要的并行程序设计原语是自旋锁(spinlock)。自旋锁的思想
就是在一个密封的循环里坚持反复尝试夺取一个资源(一把锁)直到成功为止。这
通常是通过在类似test-and-set操作之上进行循环来实现的——即,旋转(
spinning)——一直到获得该锁。
如果这听起来好像是一个二元信号量,那是因为它就是一个二元信号量。自旋锁和
二元信号量唯一的概念区别就是你不必循环等待一个信号量——你可以夺取信号量
,也可以在不能立刻得到它时放弃申请。因此,自旋锁原本是可以通过在信号量代
码外再包裹一层循环来实现的。不过,因为自旋锁是信号量的一个受限特例,它们
有更高效的实现方法。
自旋锁变量——其中的一位被测试和设置——总是spinlock_t类型(12785行)。
只有spinlock_t的最低位被使用;如果锁可用,则它是0,如果被取走,则它是1。
在一个声明里,自旋锁被初始化为值SPIN_LOCK_UNLOCKED(12789行);它也可以
用spin_lock_init函数(12791行)来初始化。这两者都把spinlock_t的lock成员
设置成0——也就是未锁状态。
注意12795行代码简洁地对公平性进行了考虑并最后抛弃了它——公平是饥饿的背
面,正如我们前面已经介绍过的(使得一个CPU或进程饥饿应被认为是“不公平的
”)。
自旋锁的加锁和解锁宏建立在spin_lock_string和sping_unlock_string函数之上
,所以这一小节只对spin_lock_string和sping_unlock_string函数进行详述。其
它宏如果有的话只是增加了IRQ加锁和解锁。
spin_lock_string
12805:这个宏的代码对于所有自旋锁加锁的宏都是相同的。它也被用于x86专用的
lock_ kernel和unlock_kernel版本之中(它们不在本书之列,不过其常规版本则
是包括的——参见10174和10182行)。
12807:尝试测试和设置自旋锁的最低位,这要把内存总线锁住以便对于任何其它
对同一个自旋锁的访问来说这个操作都是原子的。
12808:如果成功了,控制流程就继续向下运行;否则,spin_lock_string函数向
前跳转到第12810行(btsl把这一位的原值放入CPU的进位标志位(Carry flag),
这正是这里使用jc的原因)。同样的技巧我们已经看到过三次了:跳转目标放在内
核的单独一段中。
12811:在封闭的循环里不停地检测循环锁的最低位。注意btsl和testb以不同方式
解释它们第一个操作数——对于btsl,它是一个位状态(bit position),而对于
testb,它是一个位屏蔽(bitmask)。因此,12811行在测试spin_lock_string曾
在12807行已经试图设置(但失败了)的同一位,尽管一个使用$0而另一个使用$1

12813:该位被清除了,所以spin_lock_string应该再次夺取它。函数调转回第
12806行。
    这个代码可以只用加上lock前缀的两条代码加以简化:
    1: lock ; btsl $0, %0
      jc 1b
    不过,使用这个简化版本的话,系统性能将明显受到损害,这因为每次循环重复
内存总线都要被加锁。内核使用的版本虽然长一些,但是它可以使其它CPU运行的
更有效,这是由于该版本只有在它有充分理由相信能够获得锁的时候才会锁住内存
总线。
spin_unlock_string
12816:并不很重要:只是重新设置了自旋锁的锁定位(lock bit)。
读/写自旋锁
自旋锁的一个特殊情况就是读/写自旋锁。这里的思想是这样的:在某些情况中,
我们想要允许某个对象有多个读者,但是当有一个写者正在写入这个对象时,则不
允许它再有其它读者或者写者。
遵循基于spinlock_t的自旋锁的同样模式,读/写自旋锁是用rwlock_t(12853行)
来代表的,它可以在有RW_LOCK_UNLOCKED(12858行)的声明里被初始化。与
rwlock_t一起工作的最低级的宏是read_lock、read_unlock、write_lock,以及
write_unlock,它们在本小节中进行描述。很明显,那些跟随在这些宏之后并建立
在它们之上的宏,自然要在你理解了最初的这四个宏之后在去接触。
正如第12860行注释中所声明的,当写锁(write lock)被占有时,rwlock_t的
lock成员是负值。当既没有读者也没有写者时它为0,当只有读者而没有写者时它
是正值——在这种情况下,lock将对读者的数目进行计数。
read_lock
12867:开始于rwlock_t的lock成员的自动递增。这是推测性的操作——它可以被
撤销。
12868:如果它在增量之后为负,表示某个进程占用了写锁——或者至少是某个进
程正试图得到它。read_lock向前跳到第12870行(注意,在一个不同的内核段里)
。否则,没有写者退出(尽管还有可能有,或者也有可能没有其它读者——这并不
重要),所以可以继续执行读锁定(read-locked)代码。
12870:一个写者出现了。read_lock取消第12867行增值操作的影响。
12871:循环等待rwlock_t的lock变为0或正值。
12873:跳回到第12866行再次尝试。
read_unlock
12878:不太复杂:只是递减该计数值。
write_lock
12883:表示出有一个进程需要写锁:检测并设置lock的符号位并保证lock的值是
负的。
12884:如果符号位已经被设置,则另外有进程占有了写锁;write_lock向前跳转
到第12889行(同以前一样,那是在一个不同的内核段里)。
12885:没有别的进程正试图获得该写锁,可是读者仍可以退出。因为符号位被设
置了,读者不能获得读锁,但是write_lock仍然必须等待正在退出的读者完全离开
。它通过检查低端的31位中是否任何一位被设置过开始,这可以表示lock以前曾是
正值。如果没有,则lock在符号位反转之前曾是0,这意味着没有读者;因而,这
对于写者的继续工作是很安全的,所以控制流程就可以继续向下运行了。不过,如
果低端31位中任何一位被设置过了,也就是说有读者了,这样write_lock就会向前
跳转到第12888行等到它们结束。
12888:该进程是仅有的写者,但是有若干读者。write_lock会暂时清除符号位(
这个宏稍后将再次操纵它)。有趣的是,对符号位进行这样的胡乱操作并不会影响
读者操纵lock的正确性。考虑作为示例的下列顺序事件:
1.  两个读者增加了lock;lock用十六进制表示现在是0x00000002。
2.  一个即将成为写者的进程设置了符号位;lock现在是0x80000002。
3.  读者中的一个离开;lock现在是0x80000001。
4.  写者看到剩余的位不全部是0——仍然有读者存在。这样它根本没有写锁,因此
它就清除符号位;lock现在是0x00000001。
    这样,读和写可以任何顺序交错尝试操作而不会影响结果的正确程度。
12889:循环等待计数值降到0——也就是等待所有读者退出。实际上,0除了表示
所有读者已离开之外,它还表示着没有其它进程获得了写锁。
12891:所有读者和写者都结束了操作;write_lock又从头开始,并再次获得写锁

write_unlock
12896:不太重要:只是重置符号位。
APICs和CPU-To-CPU通信
Intel 多处理规范的核心就是高级可编程中断控制器(Advanced Programmable
Interrupt Controllers——APICs)的使用。CPU通过彼此发送中断来完成它们之
间的通信。通过给中断附加动作(actions),不同的CPU可以在某种程度上彼此进
行控制。每个CPU有自己的APIC(成为那个CPU的本地APIC),并且还有一个I/O
APIC来处理由I/O设备引起的中断。在普通的多处理器系统中,I/O APIC取代了第
6章里提到的中断控制器芯片组的作用。
这里有几个示例性的函数来让你了解其工作方式的风格。
smp_send_reschedule
5019: 这个函数只有一行,其作用将在本章随后进行说明,它仅仅是给其ID以参
数形式给出了的目标CPU发送一个中断。函数用CPU ID和RESCHEDULE_VECTOR向量调
用send_IPI_single函数(4937行)。RESCHEDULE_VECTOR与其它CPU中断向量是一
起在第1723行开始的一个定义块中被定义的。
send_IPI_single
4937: send_IPI_single函数发送一个IPI——那是Intel对处理器间中断(
interprocessor interrupt)的称呼——给指定的目的CPU。在这一行,内核以相
当低级的方式与发送CPU的本地APIC对话。
4949: 得到中断命令寄存器(ICR)高半段的内容——本地APIC就是通过这个寄存
器进行编程的——不过它的目的信息段要被设置为dest。尽管__prepare_ICR2(
4885行)里使用了“2”,CPU实际上只有一个ICR而不是两个。但是它是一个64位
寄存器,内核更愿意把它看作是两个32位寄存器——在内核代码里,“ICR”表示
这个寄存器的低端32位,所以“ICR2”就表示高端32位。我们想要设置的的目的信
息段就在高端32位,即ICR2里。
4950: 把修改过的信息写回ICR。现在ICR知道了目的CPU。
4953: 调用__prepare_ICR(4874行)来设置我们想要发送给目的CPU的中断向量
。(注意没有什么措施能够保证目的CPU不是当前CPU——ICR完全能够发送一个
IPI给它自己的CPU。尽管这样,我还是没有找到有任何理由要这样做。)
4957: 通过往ICR里写入新的配置来发送中断。
SMP支持如何影响内核
既然读者已经学习了能够成功支持SMP的若干原语,那么就让我们来纵览一下内核
的SMP支持吧。本章剩余的部分将局限于对分布在内核之中的那些具有代表性的
SMP代码进行讨论。
对调度的影响
schedule(26686行)正是内核的调度函数,它已在第7章中全面地介绍过了。
schedule的SMP版本与UP的相比有两个主要区别:
?   在schedule里从第26780开始的一段代码要计算某些其它地方所需的信息。
?   在SMP和UP上都要发生的对__schedule_tail的调用(26638行)实际上在UP上并
无作用,因为__schedule_tail完全是为SMP所写的代码,所以从实用的角度来说它
就是SMP所特有的。
schedule
26784:获取当前时间,也就是自从机器开机后时钟流逝的周期数。这很像是检查
jiffies,不过是以CPU周期而不是以时钟滴答作为计时方法的——显然,这要精确
得多。
26785:计算自从schedule上一次在此CPU上进行调度后过去了多长时间,并且为下
一次的计算而记录下当前周期计数。(schedule_data是每个CPU aligned_data数
组的一部分,它在26628行定义。)
26790:进程的avg_slice成员(16342行)记录该进程在其生命周期里占有CPU的平
均时间。可是这并不是简单的平均——它是加权平均,进程近期的活动远比很久以
前的活动权值大。(因为真实计算机的计算是有穷的,“很久以前”的部分在足够
远以后,将逐渐趋近于0。)这将在reschedule_idle中(26221行,下文讨论)被
用来决定是否把进程调入另一个CPU中。因此,在UP的情况下它是无需而且也不会
被计算的。
26797:记录哪一个CPU将运行next(它将在当前的CPU上被执行),并引发它的
has_cpu标志位。
26803:如果上下文环境发生了切换,schedule记录失去CPU的进程——这将在下文
的__schedule_tail中被使用到。
__schedule_tail
26654:如果失去CPU的任务已经改变了状态(这一点在前边的注释里解释过了),
它将被标记以便今后的重新调度。
26664:因为内核已经调度出了这个进程,它就不再拥有CPU了——这样的事实也将
被记录。
reschedule_idle
26221:当已经不在运行队列里的进程被唤醒时,wake_up_process将调用
reschedule_idle,进程是作为p而被传递进reschedule_idle中的。这个函数试图
把新近唤醒的进程在一个不同的CPU上进行调度——即一个空闲的CPU上。
26225:这个函数的第一部分在SMP和UP场合中都是适用的。它将使高优先级的进程
得到占用CPU的机会,同时它也会为那些处于饥饿状态的进程争取同样的机会。如
果该进程是实时的或者它的动态优先级确实比当前占有CPU进程的动态优先级要高
某个量级(强制选定的),该进程就会被标记为重新调度以便它能够争取占用CPU

26263:现在来到SMP部分,它仅仅适用于在上述测试中失败了的那些进程——虽然
这种现象经常发生。reschedule_idle必须确定是否要在另一个CPU上尝试运行该进
程。
    正如在对schedule的讨论中所提到的那样,一个进程的avg_slice成员是它对CPU
使用的加权平均值;因此,它说明了假如该进程继续运行的话是否它可能要控制
CPU一段相对来说较长的时间。
26264:这个if条件判断的第二个子句使用related宏(就在本函数之上的第26218
行)来测试是否CPU都在控制着——或想要控制——内核锁。如果是这样,那么不
管它们生存于何处,都将不大可能同时运行,这样把进程发送到另一个CPU上将不
会全面提高并行的效能。因此,假如这条子句或者前一条子句被满足,函数将不会
考虑使进程在另一CPU上进行调度并简单的返回。
26267:否则,reschedule_idle_slow(接下来讨论)被调用以决定是否进程应当
被删除。
reschedule_idle_slow
26157:正如注释中所说明的,reschedule_idle_slow试图找出一个空闲CPU来贮存
p。这个算法是基于如下观察结果的,即task数组的前n项是系统的空闲进程,机器
的n个CPU中每个都对应一个这样的空闲进程。这些空闲进程当(且仅当)对应CPU
上没有其它进程需要处理器时才会运行。如果可能,函数通常是用hlt指令使CPU进
入低功耗的“睡眠”状态。
    因此,如果有空闲CPU存在的话,对任务数组的前n个进程进行循环是找出一个空
闲CPU所必须的。reschedule_idle_slow函数只需简单的查询每个空闲进程是否此
刻正在运行着;如果是这样,它所在的CPU就一定是空闲的,这就为进程p提供了一
个很好的候选地点来运行。
    当然,这个被选中的明显空闲的CPU完全有可能只是暂时空闲而且必定会被一堆拥
有更高优先级的,CPU绑定的进程所充满,这些进程可能在一纳秒后就会被唤醒并
在该CPU上运行。所以,这并不是完美的解决方法,可是从统计的角度来说它已经
相当好了——要记住,像这样的选择是很符合调度程序“快餐店式(
quick-and-dirty)”的处理方式的。
26180:建立本地变量。best_cpu是此时正在运行的CPU;它是“最佳”的CPU,因
为p在其上会避免缓冲区溢出或其它的开销麻烦。this_cpu是运行
reschedule_idle_slow的CPU。
26182:idle和tsk将沿task数组进行遍历,target_tsk将是所找到的最后一个正在
运行的空闲进程(或者假如没有空闲进程它就为NULL)。
26183:i 从smp_num_cpus(前边被叫作n)开始并且在每一次循环后都递减。
26189:假如这个空闲进程的has_cpu标志被设置,它就正在它的CPU上运行着(我
们将称这样的CPU为“目标(target)CPU”)。如果该标志没有被设置,那么目标
CPU就正被某个其它进程占用着;因而,它也就不是空闲的,这样
reschedule_idle_slow将不会把p发送到那里。刚刚提及问题的反面在这里出现了
:现在仅因为CPU不空闲并不能表示它所有的进程都不会死亡而使其空闲下来。可
是reschedule_idle_slow无法知道这种情形,所以它最好还是假定目标CPU将要被
占用一段时间。无论如何,这都是可能的,就算并非如此,某个其它的进程也将很
快会被调度到另一个空闲CPU上运行。
26190:不过假如CPU目标就是当前CPU,它就会被跳过。这看来很怪,不过无论怎
样这都是“不可能发生”的情况:一个空闲进程的counter是负值,在第26226行的
测试将早已阻止这个函数执行到这一步了。
26192:找到一个可用的空闲CPU;相关的空闲进程被保存在target_tsk中。
    既然已找到了空闲CPU,为什么现在不中断循环呢?这是因为继续循环可能会发现
p当前所在的处理器也是空闲的,在两个CPU都空闲时,维持在当前处理器上运行要
比把它送往另一个好一些。
26193:这一步reschedule_idle_slow检查是否p所在的处理器空闲。如果刚才找到
的空闲CPU就是p所在的,函数将向前跳转到send标记处(26203行)来在那个CPU上
对p进行调度。
26199:函数已经转向另一个CPU;它要递减。
26204:如果循环遍历了所有空闲的CPU,该CPU的空闲任务就被标记为重新调度并
且smp_ send_reschedule(26205行)会给那个CPU发送一个IPI以便它可以重新对
其进程进行调度。
    正如读者所见到的,reschedule_idle_slow是CPU之间协调无需在UP系统中所进行
的工作的典范示例。对于UP机器来说,询问进程应占有哪一个CPU和询问它是否应
拥有系统的唯一的一个CPU或根本不应该占有CPU是等价的。SMP机器必须花费一些
代价来决定系统中哪一个CPU是该进程的最佳栖身之所。当然,换来的速度极大提
高使得这些额外的努力还是相当合算的。
release
22951:release中非SMP特有的部分在第7章中已经介绍过了——在这里,一个僵进
程(zombie)将被送往坟墓,而且其struct task_struct将被释放。
22960:查看是否该进程拥有一个CPU。(拥有它的CPU可能还没有清除这个标志;
但是它马上就将执行这个操作。)如果没有,release退出循环并像往常一样接着
释放struct task_ struct结构体。
22966:否则,release等待进程的has_cpu标志被清除。当它被清除后,release再
次进行尝试。这种貌似奇特的情况——某进程正被删除,然而它仍占有CPU——确
实少见,不过并非不可能。进程可能已经在一个CPU上被杀死,而且这个CPU还没来
得及清除has_cpu标志,但是它的父进程已经正在从另一个CPU对它进行释放了。
smp_local_timer_interrupt
对于UP专有的update_process_times函数(27382行)来说,这个函数就是它在
SMP上的对应。该函数能够完成update_process_times所完成的所有任务——更新
进程和内核在CPU使用方面的统计值——以及其它的一些操作。与众不同的地方在
于拥有这个特性的SMP版本并没有被添加到一个UP函数中去,而是采用了一个具有
同样功能,但却完全分离的功能程序。在浏览了函数之后,我们就能够很容易的知
道这是为什么了——它与UP版本差别甚大到以至于试图将二者融为一体都将是无意
义的。smp_local_timer_interrupt可从两个地方进行调用:
?   从smp_apic_timer_interrupt(5118行)调用,它用于SMP的时钟中断。这是通
过使用在第1856行定义的BUILD_SMP_TIMER_INTERRUPT宏于第919行建立起来的。
?   从第5776行通常的UP时钟中断函数里进行调用。只有当在UP机器上运行SMP内核
时此种调用方式才会发生。
smp_local_timer_interrupt
5059: prof_counter(4610行)用于跟踪到更新进程和内核统计值之前内核应该
等待多长时间;如果该计数器还没有到达0,控制流程会有效地跳转到函数的末尾
。正如代码中所证明的,prof_counter项目从1开始递减计数,除非由根(root)
来增加这个值,因此在缺省情况下每次时钟滴答都要完成此项工作。然后,
prof_counter[cpu]从prof_multiplier[cpu]处被重新初始化。
    明显的这是一个优化的过程:每次时钟滴答都在这个if语句块里完成所有工作将
相当的缓慢,所以我们可能想到以牺牲一些精确度的代价将工作分批完成。因为乘
法器是可调的,所以你可以指定你所需要的速度频率来放松对准确度的要求。
    然而,关于这段代码我总感到有些困惑:确定无疑的是,当
prof_multiplier[cpu]耗尽时,统计值应该被更新,就像prof_multiplier[cpu]的
计数流逝一样——既然它们已经如此。(除了prof_multiplier[cpu]本身刚刚被改
变时,不过这已经偏离了这里讨论的主题。)与此不同的是,这里代码表现出来的
就好像只经过了一次滴答计数。或许其用意是为了以后能把记录下来的滴答数目和
prof_multiplier[cpu]在某个地方相乘,不过现在并没有这样实现。
5068: 当时钟中断被触发时假如系统正在用户模式运行,
smp_local_timer_interrupt会假定全部滴答都是在用户模式里流逝的;否则,它
将假定全部滴答是在系统模式里流逝的。
5073: 用irq_enter(1792行)来夺取全局IRQ锁。这是我们要分批处理这项工作
的另一个原因:并不需要在每次时钟滴答时都要得到全局IRQ锁,这有可能成为
CPU之间争夺的一个重要根源,实际中函数是以较低的频度来争取该锁的。因此,
函数不经常夺取这个锁,可是一旦它获得了锁,就不会再使其被锁。在此我们又一
次以准确度的代价换来了这种效率上的提高。
5074: 不用为保存空闲进程的统计值而操心。这样做只会浪费CPU的周期。总之,
内核会跟踪系统处于空闲的总共时间,对空闲进程的更多细节进行统计价值不大(
比如我们知道它们总是在系统模式下执行的,所以就没有必要再明确计算它们的系
统时间了)。
5075: update_process_times和smp_local_timer_interrupt在这一点上是一致的
:它们都调用update_process_times来完成对单进程CPU使用统计的更新工作。
5077: 减少进程的counter(它的动态优先级),如果它被耗尽就重新调度该进程

5082: 更新内核的统计数字。如在update_process_times中一样,用户时间既可
以用内核的“最优时间”也可以用常规的用户时间来计算,这要取决于进程的优先
级是否低于DEF_PRIORITY。
5094: 重新初始化CPU的prof_counter并释放全局IRQ锁。该工作必须要以这种顺
序完成,当然——若以相反的方式,则可能在prof_counter被重新初始化之前发生
又一次时钟中断。
lock_kernel和unlock_kernel
这两个函数也有专门适应于x86平台的版本;但是在这里只介绍通用版本。
lock_kernel
10174:这个函数相当简单,它获得全局内核锁——在任何一对
lock_kernel/unlock_kernel函数里至多可以有一个CPU。显然这在UP机器上是一个
空操作(no-op)。
10176:进程的lock_depth成员初始为–1(参见24040行)。在它小于0时(若小于
0则恒为-1),进程不拥有内核锁;当大于或等于0时,进程得到内核锁。
    这样,单个进程可以调用lock_kernel,然后在运行到unlock_kernel之前可能又
将调用另一个要使用lock_kernel的函数。在这种情况中,进程将立刻被赋予内核
锁——而这正是我们所期望的。
    其结果是,一旦增加进程的lock_depth就会使lock_depth为0,那么进程以前就是
没有锁的。所以,函数在此情形下获得kernel_flag自旋锁(3587行)。
unlock_kernel
10182:同样的,如果丢弃内核锁就会使lock_depth低于0值,进程退出它所进入的
最后一对lock_kernel/unlock_kernel函数。此时,kernel_flag自旋锁一定要被解
锁以便其它进程可以给内核加锁。通过测试结果的符号位(即使用“<0”而不是“
== -1”)可以使gcc生成更高效的代码,除此之外,这还可能有利于内核在面对不
配对的lock_ kernel/unlock_kernel时可正确执行(或者不能,这取决于具体情况
)。
softirq_trylock
你可能能够回忆起在第6章的讨论中,softirq_trylock的作用是保证对于其它程序
段来说下半部分代码(bottom half)是原子操作——也就是说,保证在任何特定
时段的整个系统范围之内至多只有一个下半部分代码在运行。对于UP来说这相当容
易:内核只不过需要检查或者还要设置一下标志位就可以了。不过对于SMP来说自
然没有这样简单。
softirq_trylock
12528:测试并设置(tests-and-sets)global_bh_count的第0位。尽管读者可能
会从global _bh_count的名字上得到另外一种看法,实际它总是0或者1的——这样
的考虑是适当的,因为至多运行一个下半部分程序代码。不管怎样,如果
global_bh_count已经是1了,那么就已经有一个下半部分代码在运行着,因此控制
流程就跳转到函数末尾。
12529:如果还可得到global_bh_lock,那么下半部分代码就能够在这个CPU上运行
。这种情况与UP机器上使用的双锁系统非常类似。
12533:softirq_trylock无法获取global_bh_lock,因此它的工作失败了。
cli和sti
正如在第6章中解释过的,cli和sti分别用于禁止和启用中断。对于UP这简化为单
个cli或sti指令。而在SMP情况下,这就很不够了,我们不仅需要禁止本地CPU还要
暂时避免其它CPU处理IRQ。因此对于SMP,宏就变成了对__global_cli和
__global_sti函数的调用。
__global_cli
1220: 把CPU的EFLAGS寄存器复制到本地变量flags里。
1221: x86系统里的中断使能标志在EFLAGS寄存器的第9位——在第1205行解释了
EFLAG_IF_SHIFT的定义。它被用来检测是否已经禁止了中断,这样就不再需要去禁
止它们了。
1223: 禁止这个CPU的中断。
1224: 如果该CPU没有正在对IRQ进行处理,__global_cli就调用get_irqlock(
1184行)来获得全局IRQ锁。如果CPU已经在对IRQ进行处理了,那么正如我们马上
要看到的,它已经拥有了该全局IRQ锁。
    现在本CPU已经禁止了中断,而且它也拥有了全局IRQ锁,这样任务就完成了。
__global_sti
1233: 如果CPU没有正在对IRQ进行处理,__global_sti就在__global_cli中通过
release_irqlock(10752行)调用来实现对全局IRQ锁的释放工作。如果CPU已经在
对IRQ进行处理了,那么它已经拥有了该全局IRQ锁,正如在接下来的部分中将要解
释的那样,这个锁将在其它地方被释放掉。
1235: 再次允许在本CPU上进行中断。
irq_enter和irq_exit
第6章中顺便提及了这两个函数的UP版本。包含在一对irq_enter/irq_exit之中的
代码段都是原子操作,这不仅对于其它这样的代码区域是原子的,而且对于
cli/sti宏对来说也是如此。
irq_enter
1794: 调用hardirq_enter(10761行)自动为本CPU增加全局IRQ计数和本地IRQ计
数。这个函数记录了CPU正在处理一个IRQ的情况。
1795: 执行循环直到这个CPU得到全局IRQ锁为止。这就是为什么我要在前面说明
如果CPU正在处理IRQ,那么它就已经获得了全局IRQ锁的原因:到这个函数退出时
,这两个特性都将被加强。对于内核代码来说,把这两个特性分离出去并没有太大
的意义——它可以直接调用hardirq_enter,而且也不用去争夺全局IRQ锁。函数只
是没有这样作而已。
irq_exit
1802: 这个函数转向hardirq_enter的相反函数hardirq_exit(10767行)。顺便
要提及的是,对irq_enter和irq_exit来说其irq参数都被忽略了——至少在x86平
台上如此。



--
      寂寞苦寒夜,
                ........我情愿,

            焚琴煮鹤.........
※ 来源:·兰大思雨站 siyu.dhs.org·[FROM: 210.26.49.193]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店