< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31146</FONT>:与前边的情况类似,从用户空间中复制新值,而且结果以<FONT face="Times New Roman">NUL</FONT>来结束。不过在这种情形下,不从用户空间复制<FONT face="Times New Roman">NUL</FONT>字节是一种正确的做法,因为把它从用户空间复制进来要比仅仅在<FONT face="Times New Roman">data</FONT>的适当字节安排一个<FONT face="Times New Roman">NUL</FONT>效率低。而且以这种方式,即使输入不是<FONT face="Times New Roman">NUL</FONT>结束的,<B normal"><FONT face="Times New Roman">table->data</FONT></B>也要如此。当然,从<B normal"><FONT face="Times New Roman">newval</FONT></B>读出的字符串可能已经是<FONT face="Times New Roman">NUL</FONT>结束的,在那种情况里第<FONT face="Times New Roman">31154</FONT>行的赋值就是多余的。这还是另一种情况,直接完成工作比检查需要是否执行它还要快。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31156</FONT>:返回<FONT face="Times New Roman">0</FONT>表示成功。相反,返回的值应该为正数,以便<FONT face="Times New Roman">30618</FONT>行代码认为结果是成功的。而又相反,调用代码认为<B normal"><FONT face="Times New Roman">sysctl_string</FONT></B>想让缺省处理发生,然后它就继续从用户空间再次复制多余的数据。</P>< 12pt 0cm 3.2pt"><b>sysctl_intvec</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31163</FONT>:<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>是在<FONT face="Times New Roman">kernel/sysctl.c</FONT>里定义的另一个策略例程。它确保假如调用程序正在写入表项,所有被写的<B normal"><FONT face="Times New Roman">int</FONT></B>都应位于某个最小和最大值之间。(顺便提及一下,<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>在这个文件里只被使用了一次——在第<FONT face="Times New Roman">30414</FONT>行——尽管它被广泛的用于本书所没有包括的内核的其它代码之中。)</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31170</FONT>:如果新的欲写数据总量不符合一个<B normal"><FONT face="Times New Roman">int</FONT></B>大小的边界,它就无效,所以尝试被抛弃。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31173</FONT>:假如表项没有指定一组最大或最小值,输入的值就永远不可能超出范围,这样调用程序里的普通写代码(<B normal"><FONT face="Times New Roman">do_sysctl_strategy</FONT></B>,<FONT face="Times New Roman">30603</FONT>行)就足够好了。因此在这种情况里<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>返回<FONT face="Times New Roman">0</FONT>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31184</FONT>:进行循环以确保所有来自输入数组的值都位于适当范围之内。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31186</FONT>:这行代码不检查<B normal"><FONT face="Times New Roman">get_user</FONT></B>的返回值——没有迫切的需要去这样做。如果当不能读取一个输入内存位置时,<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>返回<FONT face="Times New Roman">0</FONT>(成功),那么当它试图读取整个数组时<B normal"><FONT face="Times New Roman">do_sysctl_strategy</FONT></B>就会注意到这个问题。作为另一选择,假如<B normal"><FONT face="Times New Roman">get_user</FONT></B>无法读取内存位置,无用信息(<FONT face="Times New Roman">garbage</FONT>)可能在<B normal"><FONT face="Times New Roman">value</FONT></B>里结束并且数值可能会不正确的被抛弃。在此情况里,调用程序将得到一个<B normal"><FONT face="Times New Roman">EINVAL</FONT></B>错误而不是<B normal"><FONT face="Times New Roman">EFAULT</FONT></B>错误,这只是一个小缺陷(<FONT face="Times New Roman">bug</FONT>)。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31187</FONT>:注意这一行不会被折磨第<FONT face="Times New Roman">31033</FONT>行相似代码的缺陷所困扰,该行中在最小值和最大值之上进行的并行循环会产生不同步的情况。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这一行代码能够避免位于<FONT face="Times New Roman">31033</FONT>行的缺陷被暴露出来。正如实际中所进行的,<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>和<B normal"><FONT face="Times New Roman">proc_dointvec_minmax</FONT></B>都总是与同一个<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>条目相关联的。因此,在调用处理例程(<FONT face="Times New Roman">handler routine</FONT>)<B normal"><FONT face="Times New Roman">proc_dointvec_minmax</FONT></B>之前,任何超出允许范围之外的数值将会被策略例程<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>截获。所以,我们知道——在给定内核里所有的<B normal"><FONT face="Times New Roman">ctl_tables</FONT></B>最新定义的情况下——<B normal"><FONT face="Times New Roman">proc_dointvec_minmax</FONT></B>将永远不会遇到超出界限的数值,而那里是唯一可以触发该缺陷的数值种类。某个调用程序或许可以注册一个使用<B normal"><FONT face="Times New Roman">proc_dointvec_minmax</FONT></B>但没有策略例程的<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>,但是尽管这样,在<B normal"><FONT face="Times New Roman">proc_dointvec_ minmax</FONT></B>里的这个缺陷迟早会造成一定损害。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31193</FONT>:返回<FONT face="Times New Roman">0</FONT>表示成功。这里不像在第<FONT face="Times New Roman">31156</FONT>行那样是一个错误,因为<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>并不向<B normal"><FONT face="Times New Roman">table->data</FONT></B>里写入。从用户空间读出的值只是被读进一个临时变量里作范围检查,然后就被删除;<B normal"><FONT face="Times New Roman">do_sysctl_strategy</FONT></B>将完成那项工作,并只向<B normal"><FONT face="Times New Roman">table->data</FONT></B>里进行写入。</P>
< 12pt 0cm 3.2pt"><b>send_IPI_single</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">4937</FONT>:<FONT face="Times New Roman"> <B normal">send_IPI_single</B></FONT>函数发送一个<FONT face="Times New Roman">IPI</FONT>——那是<FONT face="Times New Roman">Intel</FONT>对处理器间中断(<FONT face="Times New Roman">interprocessor interrupt</FONT>)的称呼——给指定的目的<FONT face="Times New Roman">CPU</FONT>。在这一行,内核以相当低级的方式与发送<FONT face="Times New Roman">CPU</FONT>的本地<FONT face="Times New Roman">APIC</FONT>对话。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">4949</FONT>:<FONT face="Times New Roman"> </FONT>得到中断命令寄存器(<FONT face="Times New Roman">ICR</FONT>)高半段的内容——本地<FONT face="Times New Roman">APIC</FONT>就是通过这个寄存器进行编程的——不过它的目的信息段要被设置为<B normal"><FONT face="Times New Roman">dest</FONT></B>。尽管<B normal"><FONT face="Times New Roman">__prepare_ICR2</FONT></B>(<FONT face="Times New Roman">4885</FONT>行)里使用了“<FONT face="Times New Roman">2</FONT>”,<FONT face="Times New Roman">CPU</FONT>实际上只有一个<FONT face="Times New Roman">ICR</FONT>而不是两个。但是它是一个<FONT face="Times New Roman">64</FONT>位寄存器,内核更愿意把它看作是两个<FONT face="Times New Roman">32</FONT>位寄存器——在内核代码里,“<FONT face="Times New Roman">ICR</FONT>”表示这个寄存器的低端<FONT face="Times New Roman">32</FONT>位,所以“<FONT face="Times New Roman">ICR2</FONT>”就表示高端<FONT face="Times New Roman">32</FONT>位。我们想要设置的的目的信息段就在高端<FONT face="Times New Roman">32</FONT>位,即<FONT face="Times New Roman">ICR2</FONT>里。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">4950</FONT>:<FONT face="Times New Roman"> </FONT>把修改过的信息写回<FONT face="Times New Roman">ICR</FONT>。现在<FONT face="Times New Roman">ICR</FONT>知道了目的<FONT face="Times New Roman">CPU</FONT>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">4953</FONT>:<FONT face="Times New Roman"> </FONT>调用<B normal"><FONT face="Times New Roman">__prepare_ICR</FONT></B>(<FONT face="Times New Roman">4874</FONT>行)来设置我们想要发送给目的<FONT face="Times New Roman">CPU</FONT>的中断向量。(注意没有什么措施能够保证目的<FONT face="Times New Roman">CPU</FONT>不是当前<FONT face="Times New Roman">CPU</FONT>——<FONT face="Times New Roman">ICR</FONT>完全能够发送一个<FONT face="Times New Roman">IPI</FONT>给它自己的<FONT face="Times New Roman">CPU</FONT>。尽管这样,我还是没有找到有任何理由要这样做。)</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">4957</FONT>:<FONT face="Times New Roman"> </FONT>通过往<FONT face="Times New Roman">ICR</FONT>里写入新的配置来发送中断。</P><H2 13pt 0cm">SMP支持如何影响内核</H2>< 0cm 0cm 0pt">既然读者已经学习了能够成功支持<FONT face="Times New Roman">SMP</FONT>的若干原语,那么就让我们来纵览一下内核的<FONT face="Times New Roman">SMP</FONT>支持吧。本章剩余的部分将局限于对分布在内核之中的那些具有代表性的<FONT face="Times New Roman">SMP</FONT>代码进行讨论。</P><H3 13pt 0cm"><FONT size=5>对调度的影响</FONT></H3>< 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">schedule</FONT></B>(<FONT face="Times New Roman">26686</FONT>行)正是内核的调度函数,它已在第<FONT face="Times New Roman">7</FONT>章中全面地介绍过了。<B normal"><FONT face="Times New Roman">schedule</FONT></B>的<FONT face="Times New Roman">SMP</FONT>版本与<FONT face="Times New Roman">UP</FONT>的相比有两个主要区别:</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo1; tab-stops: list 42.25pt">l 在<B normal"><FONT face="Times New Roman">schedule</FONT></B>里从第<FONT face="Times New Roman">26780</FONT>开始的一段代码要计算某些其它地方所需的信息。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo1; tab-stops: list 42.25pt">l 在<FONT face="Times New Roman">SMP</FONT>和<FONT face="Times New Roman">UP</FONT>上都要发生的对<B normal"><FONT face="Times New Roman">__schedule_tail</FONT></B>的调用(<FONT face="Times New Roman">26638</FONT>行)实际上在<FONT face="Times New Roman">UP</FONT>上并无作用,因为<B normal"><FONT face="Times New Roman">__schedule_tail</FONT></B>完全是为<FONT face="Times New Roman">SMP</FONT>所写的代码,所以从实用的角度来说它就是<FONT face="Times New Roman">SMP</FONT>所特有的。</P>< 12pt 0cm 3.2pt"><b>schedule</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26784</FONT>:获取当前时间,也就是自从机器开机后时钟流逝的周期数。这很像是检查<B normal"><FONT face="Times New Roman">jiffies</FONT></B>,不过是以<FONT face="Times New Roman">CPU</FONT>周期而不是以时钟滴答作为计时方法的——显然,这要精确得多。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26785</FONT>:计算自从<B normal"><FONT face="Times New Roman">schedule</FONT></B>上一次在此<FONT face="Times New Roman">CPU</FONT>上进行调度后过去了多长时间,并且为下一次的计算而记录下当前周期计数。(<B normal"><FONT face="Times New Roman">schedule_data</FONT></B>是每个<FONT face="Times New Roman">CPU <B normal">aligned_data</B></FONT>数组的一部分,它在<FONT face="Times New Roman">26628</FONT>行定义。)</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26790</FONT>:进程的<B normal"><FONT face="Times New Roman">avg_slice</FONT></B>成员(<FONT face="Times New Roman">16342</FONT>行)记录该进程在其生命周期里占有<FONT face="Times New Roman">CPU</FONT>的平均时间。可是这并不是简单的平均——它是加权平均,进程近期的活动远比很久以前的活动权值大。(因为真实计算机的计算是有穷的,“很久以前”的部分在足够远以后,将逐渐趋近于<FONT face="Times New Roman">0</FONT>。)这将在<B normal"><FONT face="Times New Roman">reschedule_idle</FONT></B>中(<FONT face="Times New Roman">26221</FONT>行,下文讨论)被用来决定是否把进程调入另一个<FONT face="Times New Roman">CPU</FONT>中。因此,在<FONT face="Times New Roman">UP</FONT>的情况下它是无需而且也不会被计算的。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26797</FONT>:记录哪一个<FONT face="Times New Roman">CPU</FONT>将运行<B normal"><FONT face="Times New Roman">next</FONT></B>(它将在当前的<FONT face="Times New Roman">CPU</FONT>上被执行),并引发它的<B normal"><FONT face="Times New Roman">has_cpu</FONT></B>标志位。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26803</FONT>:如果上下文环境发生了切换,<B normal"><FONT face="Times New Roman">schedule</FONT></B>记录失去<FONT face="Times New Roman">CPU</FONT>的进程——这将在下文的<B normal"><FONT face="Times New Roman">__schedule_tail</FONT></B>中被使用到。</P>< 12pt 0cm 3.2pt"><b>__schedule_tail</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26654</FONT>:如果失去<FONT face="Times New Roman">CPU</FONT>的任务已经改变了状态(这一点在前边的注释里解释过了),它将被标记以便今后的重新调度。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26664</FONT>:因为内核已经调度出了这个进程,它就不再拥有<FONT face="Times New Roman">CPU</FONT>了——这样的事实也将被记录。</P>< 12pt 0cm 3.2pt"><b>reschedule_idle</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26221</FONT>:当已经不在运行队列里的进程被唤醒时,<B normal"><FONT face="Times New Roman">wake_up_process</FONT></B>将调用<B normal"><FONT face="Times New Roman">reschedule_idle</FONT></B>,进程是作为<B normal"><FONT face="Times New Roman">p</FONT></B>而被传递进<B normal"><FONT face="Times New Roman">reschedule_idle</FONT></B>中的。这个函数试图把新近唤醒的进程在一个不同的<FONT face="Times New Roman">CPU</FONT>上进行调度——即一个空闲的<FONT face="Times New Roman">CPU</FONT>上。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26225</FONT>:这个函数的第一部分在<FONT face="Times New Roman">SMP</FONT>和<FONT face="Times New Roman">UP</FONT>场合中都是适用的。它将使高优先级的进程得到占用<FONT face="Times New Roman">CPU</FONT>的机会,同时它也会为那些处于饥饿状态的进程争取同样的机会。如果该进程是实时的或者它的动态优先级确实比当前占有<FONT face="Times New Roman">CPU</FONT>进程的动态优先级要高某个量级(强制选定的),该进程就会被标记为重新调度以便它能够争取占用<FONT face="Times New Roman">CPU</FONT>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26263</FONT>:现在来到<FONT face="Times New Roman">SMP</FONT>部分,它仅仅适用于在上述测试中失败了的那些进程——虽然这种现象经常发生。<B normal"><FONT face="Times New Roman">reschedule_idle</FONT></B>必须确定是否要在另一个<FONT face="Times New Roman">CPU</FONT>上尝试运行该进程。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>正如在对<B normal"><FONT face="Times New Roman">schedule</FONT></B>的讨论中所提到的那样,一个进程的<B normal"><FONT face="Times New Roman">avg_slice</FONT></B>成员是它对<FONT face="Times New Roman">CPU</FONT>使用的加权平均值;因此,它说明了假如该进程继续运行的话是否它可能要控制<FONT face="Times New Roman">CPU</FONT>一段相对来说较长的时间。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26264</FONT>:这个<B normal"><FONT face="Times New Roman">if</FONT></B>条件判断的第二个子句使用<B normal"><FONT face="Times New Roman">related</FONT></B>宏(就在本函数之上的第<FONT face="Times New Roman">26218</FONT>行)来测试是否<FONT face="Times New Roman">CPU</FONT>都在控制着——或想要控制——内核锁。如果是这样,那么不管它们生存于何处,都将不大可能同时运行,这样把进程发送到另一个<FONT face="Times New Roman">CPU</FONT>上将不会全面提高并行的效能。因此,假如这条子句或者前一条子句被满足,函数将不会考虑使进程在另一<FONT face="Times New Roman">CPU</FONT>上进行调度并简单的返回。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26267</FONT>:否则,<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>(接下来讨论)被调用以决定是否进程应当被删除。</P>< 12pt 0cm 3.2pt"><b>reschedule_idle_slow</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26157</FONT>:正如注释中所说明的,<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>试图找出一个空闲<FONT face="Times New Roman">CPU</FONT>来贮存<B normal"><FONT face="Times New Roman">p</FONT></B>。这个算法是基于如下观察结果的,即<B normal"><FONT face="Times New Roman">task</FONT></B>数组的前<FONT face="Times New Roman">n</FONT>项是系统的空闲进程,机器的<FONT face="Times New Roman">n</FONT>个<FONT face="Times New Roman">CPU</FONT>中每个都对应一个这样的空闲进程。这些空闲进程当(且仅当)对应<FONT face="Times New Roman">CPU</FONT>上没有其它进程需要处理器时才会运行。如果可能,函数通常是用<B normal"><FONT face="Times New Roman">hlt</FONT></B>指令使<FONT face="Times New Roman">CPU</FONT>进入低功耗的“睡眠”状态。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>因此,如果有空闲<FONT face="Times New Roman">CPU</FONT>存在的话,对任务数组的前<FONT face="Times New Roman">n</FONT>个进程进行循环是找出一个空闲<FONT face="Times New Roman">CPU</FONT>所必须的。<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>函数只需简单的查询每个空闲进程是否此刻正在运行着;如果是这样,它所在的<FONT face="Times New Roman">CPU</FONT>就一定是空闲的,这就为进程<B normal"><FONT face="Times New Roman">p</FONT></B>提供了一个很好的候选地点来运行。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>当然,这个被选中的明显空闲的<FONT face="Times New Roman">CPU</FONT>完全有可能只是暂时空闲而且必定会被一堆拥有更高优先级的,<FONT face="Times New Roman">CPU</FONT>绑定的进程所充满,这些进程可能在一纳秒后就会被唤醒并在该<FONT face="Times New Roman">CPU</FONT>上运行。所以,这并不是完美的解决方法,可是从统计的角度来说它已经相当好了——要记住,像这样的选择是很符合调度程序“快餐店式(<FONT face="Times New Roman">quick-and-dirty</FONT>)”的处理方式的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26180</FONT>:建立本地变量。<B normal"><FONT face="Times New Roman">best_cpu</FONT></B>是此时正在运行的<FONT face="Times New Roman">CPU</FONT>;它是“最佳”的<FONT face="Times New Roman">CPU</FONT>,因为<B normal"><FONT face="Times New Roman">p</FONT></B>在其上会避免缓冲区溢出或其它的开销麻烦<B normal">。</B><B normal"><FONT face="Times New Roman">this_cpu</FONT></B>是运行<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>的<FONT face="Times New Roman">CPU</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26182</FONT>:<B normal"><FONT face="Times New Roman">idle</FONT></B>和<B normal"><FONT face="Times New Roman">tsk</FONT></B>将沿<B normal"><FONT face="Times New Roman">task</FONT></B>数组进行遍历,<B normal"><FONT face="Times New Roman">target_tsk</FONT></B>将是所找到的最后一个正在运行的空闲进程(或者假如没有空闲进程它就为<B normal"><FONT face="Times New Roman">NULL</FONT></B>)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26183</FONT>:<FONT face="Times New Roman"><B normal">i</B> </FONT>从<B normal"><FONT face="Times New Roman">smp_num_cpus</FONT></B>(前边被叫作<FONT face="Times New Roman">n</FONT>)开始并且在每一次循环后都递减。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26189</FONT>:假如这个空闲进程的<B normal"><FONT face="Times New Roman">has_cpu</FONT></B>标志被设置,它就正在它的<FONT face="Times New Roman">CPU</FONT>上运行着(我们将称这样的<FONT face="Times New Roman">CPU</FONT>为“目标(<FONT face="Times New Roman">target</FONT>)<FONT face="Times New Roman">CPU</FONT>”)。如果该标志没有被设置,那么目标<FONT face="Times New Roman">CPU</FONT>就正被某个其它进程占用着;因而,它也就不是空闲的,这样<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>将不会把<B normal"><FONT face="Times New Roman">p</FONT></B>发送到那里。刚刚提及问题的反面在这里出现了:现在仅因为<FONT face="Times New Roman">CPU</FONT>不空闲并不能表示它所有的进程都不会死亡而使其空闲下来。可是<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>无法知道这种情形,所以它最好还是假定目标<FONT face="Times New Roman">CPU</FONT>将要被占用一段时间。无论如何,这都是可能的,就算并非如此,某个其它的进程也将很快会被调度到另一个空闲<FONT face="Times New Roman">CPU</FONT>上运行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26190</FONT>:不过假如<FONT face="Times New Roman">CPU</FONT>目标就是当前<FONT face="Times New Roman">CPU</FONT>,它就会被跳过。这看来很怪,不过无论怎样这都是“不可能发生”的情况:一个空闲进程的<B normal"><FONT face="Times New Roman">counter</FONT></B>是负值,在第<FONT face="Times New Roman">26226</FONT>行的测试将早已阻止这个函数执行到这一步了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26192</FONT>:找到一个可用的空闲<FONT face="Times New Roman">CPU</FONT>;相关的空闲进程被保存在<B normal"><FONT face="Times New Roman">target_tsk</FONT></B>中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>既然已找到了空闲<FONT face="Times New Roman">CPU</FONT>,为什么现在不中断循环呢?这是因为继续循环可能会发现<B normal"><FONT face="Times New Roman">p</FONT></B>当前所在的处理器也是空闲的,在两个<FONT face="Times New Roman">CPU</FONT>都空闲时,维持在当前处理器上运行要比把它送往另一个好一些。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26193</FONT>:这一步<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>检查是否<B normal"><FONT face="Times New Roman">p</FONT></B>所在的处理器空闲。如果刚才找到的空闲<FONT face="Times New Roman">CPU</FONT>就是<B normal"><FONT face="Times New Roman">p</FONT></B>所在的,函数将向前跳转到<B normal"><FONT face="Times New Roman">send</FONT></B>标记处(<FONT face="Times New Roman">26203</FONT>行)来在那个<FONT face="Times New Roman">CPU</FONT>上对<B normal"><FONT face="Times New Roman">p</FONT></B>进行调度。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26199</FONT>:函数已经转向另一个<FONT face="Times New Roman">CPU</FONT>;它要递减。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26204</FONT>:如果循环遍历了所有空闲的<FONT face="Times New Roman">CPU</FONT>,该<FONT face="Times New Roman">CPU</FONT>的空闲任务就被标记为重新调度并且<B normal"><FONT face="Times New Roman">smp_ send_reschedule</FONT></B>(<FONT face="Times New Roman">26205</FONT>行)会给那个<FONT face="Times New Roman">CPU</FONT>发送一个<FONT face="Times New Roman">IPI</FONT>以便它可以重新对其进程进行调度。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>正如读者所见到的,<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>是<FONT face="Times New Roman">CPU</FONT>之间协调无需在<FONT face="Times New Roman">UP</FONT>系统中所进行的工作的典范示例。对于<FONT face="Times New Roman">UP</FONT>机器来说,询问进程应占有哪一个<FONT face="Times New Roman">CPU</FONT>和询问它是否应拥有系统的唯一的一个<FONT face="Times New Roman">CPU</FONT>或根本不应该占有<FONT face="Times New Roman">CPU</FONT>是等价的。<FONT face="Times New Roman">SMP</FONT>机器必须花费一些代价来决定系统中哪一个<FONT face="Times New Roman">CPU</FONT>是该进程的最佳栖身之所。当然,换来的速度极大提高使得这些额外的努力还是相当合算的。</P><P 12pt 0cm 3.2pt"><b>release</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22951</FONT>:<B normal"><FONT face="Times New Roman">release</FONT></B>中非<FONT face="Times New Roman">SMP</FONT>特有的部分在第<FONT face="Times New Roman">7</FONT>章中已经介绍过了——在这里,一个僵进程(<FONT face="Times New Roman">zombie</FONT>)将被送往坟墓,而且其<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>将被释放。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22960</FONT>:查看是否该进程拥有一个<FONT face="Times New Roman">CPU</FONT>。(拥有它的<FONT face="Times New Roman">CPU</FONT>可能还没有清除这个标志;但是它马上就将执行这个操作。)如果没有,<B normal"><FONT face="Times New Roman">release</FONT></B>退出循环并像往常一样接着释放<B normal"><FONT face="Times New Roman">struct task_ struct</FONT></B>结构体。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22966</FONT>:否则,<B normal"><FONT face="Times New Roman">release</FONT></B>等待进程的<B normal"><FONT face="Times New Roman">has_cpu</FONT></B>标志被清除。当它被清除后,<B normal"><FONT face="Times New Roman">release</FONT></B>再次进行尝试。这种貌似奇特的情况——某进程正被删除,然而它仍占有<FONT face="Times New Roman">CPU</FONT>——确实少见,不过并非不可能。进程可能已经在一个<FONT face="Times New Roman">CPU</FONT>上被杀死,而且这个<FONT face="Times New Roman">CPU</FONT>还没来得及清除<B normal"><FONT face="Times New Roman">has_cpu</FONT></B>标志,但是它的父进程已经正在从另一个<FONT face="Times New Roman">CPU</FONT>对它进行释放了。</P><H4 14pt 0cm 14.5pt"><FONT size=5>smp_local_timer_interrupt</FONT></H4><P 0cm 0cm 0pt">对于<FONT face="Times New Roman">UP</FONT>专有的<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>函数(<FONT face="Times New Roman">27382</FONT>行)来说,这个函数就是它在<FONT face="Times New Roman">SMP</FONT>上的对应。该函数能够完成<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>所完成的所有任务——更新进程和内核在<FONT face="Times New Roman">CPU</FONT>使用方面的统计值——以及其它的一些操作。与众不同的地方在于拥有这个特性的<FONT face="Times New Roman">SMP</FONT>版本并没有被添加到一个<FONT face="Times New Roman">UP</FONT>函数中去,而是采用了一个具有同样功能,但却完全分离的功能程序。在浏览了函数之后,我们就能够很容易的知道这是为什么了——它与<FONT face="Times New Roman">UP</FONT>版本差别甚大到以至于试图将二者融为一体都将是无意义的。<B normal"><FONT face="Times New Roman">smp_local_timer_interrupt</FONT></B>可从两个地方进行调用:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo2; tab-stops: list 42.25pt">l 从<B normal"><FONT face="Times New Roman">smp_apic_timer_interrupt</FONT></B>(<FONT face="Times New Roman">5118</FONT>行)调用,它用于<FONT face="Times New Roman">SMP</FONT>的时钟中断。这是通过使用在第<FONT face="Times New Roman">1856</FONT>行定义的<B normal"><FONT face="Times New Roman">BUILD_SMP_TIMER_INTERRUPT</FONT></B>宏于第<FONT face="Times New Roman">919</FONT>行建立起来的。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo3; tab-stops: list 42.25pt">l 从第<FONT face="Times New Roman">5776</FONT>行通常的<FONT face="Times New Roman">UP</FONT>时钟中断函数里进行调用。只有当在<FONT face="Times New Roman">UP</FONT>机器上运行<FONT face="Times New Roman">SMP</FONT>内核时此种调用方式才会发生。</P><P 12pt 0cm 3.2pt"><b>smp_local_timer_interrupt</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5059</FONT>:<FONT face="Times New Roman"> <B normal">prof_counter</B></FONT>(<FONT face="Times New Roman">4610</FONT>行)用于跟踪到更新进程和内核统计值之前内核应该等待多长时间;如果该计数器还没有到达<FONT face="Times New Roman">0</FONT>,控制流程会有效地跳转到函数的末尾。正如代码中所证明的,<B normal"><FONT face="Times New Roman">prof_counter</FONT></B>项目从<FONT face="Times New Roman">1</FONT>开始递减计数,除非由根(<FONT face="Times New Roman">root</FONT>)来增加这个值,因此在缺省情况下每次时钟滴答都要完成此项工作。然后,<B normal"><FONT face="Times New Roman">prof_counter[cpu]</FONT></B>从<B normal"><FONT face="Times New Roman">prof_multiplier[cpu]</FONT></B>处被重新初始化。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>明显的这是一个优化的过程:每次时钟滴答都在这个<B normal"><FONT face="Times New Roman">if</FONT></B>语句块里完成所有工作将相当的缓慢,所以我们可能想到以牺牲一些精确度的代价将工作分批完成。因为乘法器是可调的,所以你可以指定你所需要的速度频率来放松对准确度的要求。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>然而,关于这段代码我总感到有些困惑:确定无疑的是,当<B normal"><FONT face="Times New Roman">prof_multiplier[cpu]</FONT></B>耗尽时,统计值应该被更新,就像<B normal"><FONT face="Times New Roman">prof_multiplier[cpu]</FONT></B>的计数流逝一样——既然它们已经如此。(除了<B normal"><FONT face="Times New Roman">prof_multiplier[cpu]</FONT></B>本身刚刚被改变时,不过这已经偏离了这里讨论的主题。)与此不同的是,这里代码表现出来的就好像只经过了一次滴答计数。或许其用意是为了以后能把记录下来的滴答数目和<B normal"><FONT face="Times New Roman">prof_multiplier[cpu]</FONT></B>在某个地方相乘,不过现在并没有这样实现。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5068</FONT>:<FONT face="Times New Roman"> </FONT>当时钟中断被触发时假如系统正在用户模式运行,<B normal"><FONT face="Times New Roman">smp_local_timer_interrupt</FONT></B>会假定全部滴答都是在用户模式里流逝的;否则,它将假定全部滴答是在系统模式里流逝的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5073</FONT>:<FONT face="Times New Roman"> </FONT>用<B normal"><FONT face="Times New Roman">irq_enter</FONT></B>(<FONT face="Times New Roman">1792</FONT>行)来夺取全局<FONT face="Times New Roman">IRQ</FONT>锁。这是我们要分批处理这项工作的另一个原因:并不需要在每次时钟滴答时都要得到全局<FONT face="Times New Roman">IRQ</FONT>锁,这有可能成为<FONT face="Times New Roman">CPU</FONT>之间争夺的一个重要根源,实际中函数是以较低的频度来争取该锁的。因此,函数不经常夺取这个锁,可是一旦它获得了锁,就不会再使其被锁。在此我们又一次以准确度的代价换来了这种效率上的提高。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5074</FONT>:<FONT face="Times New Roman"> </FONT>不用为保存空闲进程的统计值而操心。这样做只会浪费<FONT face="Times New Roman">CPU</FONT>的周期。总之,内核会跟踪系统处于空闲的总共时间,对空闲进程的更多细节进行统计价值不大(比如我们知道它们总是在系统模式下执行的,所以就没有必要再明确计算它们的系统时间了)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5075</FONT>:<FONT face="Times New Roman"> <B normal">update_process_times</B></FONT>和<B normal"><FONT face="Times New Roman">smp_local_timer_interrupt</FONT></B>在这一点上是一致的:它们都调用<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>来完成对单进程<FONT face="Times New Roman">CPU</FONT>使用统计的更新工作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5077</FONT>:<FONT face="Times New Roman"> </FONT>减少进程的<B normal"><FONT face="Times New Roman">counter</FONT></B>(它的动态优先级),如果它被耗尽就重新调度该进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5082</FONT>:<FONT face="Times New Roman"> </FONT>更新内核的统计数字。如在<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>中一样,用户时间既可以用内核的“最优时间”也可以用常规的用户时间来计算,这要取决于进程的优先级是否低于<B normal"><FONT face="Times New Roman">DEF_PRIORITY</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5094</FONT>:<FONT face="Times New Roman"> </FONT>重新初始化<FONT face="Times New Roman">CPU</FONT>的<B normal"><FONT face="Times New Roman">prof_counter</FONT></B>并释放全局<FONT face="Times New Roman">IRQ</FONT>锁。该工作必须要以这种顺序完成,当然——若以相反的方式,则可能在<B normal"><FONT face="Times New Roman">prof_counter</FONT></B>被重新初始化之前发生又一次时钟中断。</P><H4 14pt 0cm 14.5pt"><FONT size=5>lock_kernel和unlock_kernel</FONT></H4><P 0cm 0cm 0pt">这两个函数也有专门适应于<FONT face="Times New Roman">x86</FONT>平台的版本;但是在这里只介绍通用版本。</P><P 12pt 0cm 3.2pt"><b>lock_kernel</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10174</FONT>:这个函数相当简单,它获得全局内核锁——在任何一对<B normal"><FONT face="Times New Roman">lock_kernel/unlock_kernel</FONT></B>函数里至多可以有一个<FONT face="Times New Roman">CPU</FONT>。显然这在<FONT face="Times New Roman">UP</FONT>机器上是一个空操作(<FONT face="Times New Roman">no-op</FONT>)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10176</FONT>:进程的<B normal"><FONT face="Times New Roman">lock_depth</FONT></B>成员初始为<FONT face="Times New Roman">–1</FONT>(参见<FONT face="Times New Roman">24040</FONT>行)。在它小于<FONT face="Times New Roman">0</FONT>时(若小于<FONT face="Times New Roman">0</FONT>则恒为<FONT face="Times New Roman">-1</FONT>),进程不拥有内核锁;当大于或等于<FONT face="Times New Roman">0</FONT>时,进程得到内核锁。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这样,单个进程可以调用<B normal"><FONT face="Times New Roman">lock_kernel</FONT></B>,然后在运行到<B normal"><FONT face="Times New Roman">unlock_kernel</FONT></B>之前可能又将调用另一个要使用<B normal"><FONT face="Times New Roman">lock_kernel</FONT></B>的函数。在这种情况中,进程将立刻被赋予内核锁——而这正是我们所期望的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>其结果是,一旦增加进程的<B normal"><FONT face="Times New Roman">lock_depth</FONT></B>就会使<B normal"><FONT face="Times New Roman">lock_depth</FONT></B>为<FONT face="Times New Roman">0</FONT>,那么进程以前就是没有锁的。所以,函数在此情形下获得<B normal"><FONT face="Times New Roman">kernel_flag</FONT></B>自旋锁(<FONT face="Times New Roman">3587</FONT>行)。</P><P 12pt 0cm 3.2pt"><b>unlock_kernel</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10182</FONT>:同样的,如果丢弃内核锁就会使<B normal"><FONT face="Times New Roman">lock_depth</FONT></B>低于<FONT face="Times New Roman">0</FONT>值,进程退出它所进入的最后一对<B normal"><FONT face="Times New Roman">lock_kernel/unlock_kernel</FONT></B>函数。此时,<B normal"><FONT face="Times New Roman">kernel_flag</FONT></B>自旋锁一定要被解锁以便其它进程可以给内核加锁。通过测试结果的符号位(即使用“<FONT face="Times New Roman"><0</FONT>”而不是“<FONT face="Times New Roman">== -1</FONT>”)可以使<FONT face="Times New Roman">gcc</FONT>生成更高效的代码,除此之外,这还可能有利于内核在面对不配对的<B normal"><FONT face="Times New Roman">lock_ kernel/unlock_kernel</FONT></B>时可正确执行(或者不能,这取决于具体情况)。</P><H4 14pt 0cm 14.5pt"><FONT size=5>softirq_trylock</FONT></H4><P 0cm 0cm 0pt">你可能能够回忆起在第<FONT face="Times New Roman">6</FONT>章的讨论中,<B normal"><FONT face="Times New Roman">softirq_trylock</FONT></B>的作用是保证对于其它程序段来说下半部分代码(<FONT face="Times New Roman">bottom half</FONT>)是原子操作——也就是说,保证在任何特定时段的整个系统范围之内至多只有一个下半部分代码在运行。对于<FONT face="Times New Roman">UP</FONT>来说这相当容易:内核只不过需要检查或者还要设置一下标志位就可以了。不过对于<FONT face="Times New Roman">SMP</FONT>来说自然没有这样简单。</P><P 12pt 0cm 3.2pt"><b>softirq_trylock</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12528</FONT>:测试并设置(<FONT face="Times New Roman">tests-and-sets</FONT>)<B normal"><FONT face="Times New Roman">global_bh_count</FONT></B>的第<FONT face="Times New Roman">0</FONT>位。尽管读者可能会从<B normal"><FONT face="Times New Roman">global _bh_count</FONT></B>的名字上得到另外一种看法,实际它总是<FONT face="Times New Roman">0</FONT>或者<FONT face="Times New Roman">1</FONT>的——这样的考虑是适当的,因为至多运行一个下半部分程序代码。不管怎样,如果<B normal"><FONT face="Times New Roman">global_bh_count</FONT></B>已经是<FONT face="Times New Roman">1</FONT>了,那么就已经有一个下半部分代码在运行着,因此控制流程就跳转到函数末尾。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12529</FONT>:如果还可得到<B normal"><FONT face="Times New Roman">global_bh_lock</FONT></B>,那么下半部分代码就能够在这个<FONT face="Times New Roman">CPU</FONT>上运行。这种情况与<FONT face="Times New Roman">UP</FONT>机器上使用的双锁系统非常类似。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12533</FONT>:<B normal"><FONT face="Times New Roman">softirq_trylock</FONT></B>无法获取<B normal"><FONT face="Times New Roman">global_bh_lock</FONT></B>,因此它的工作失败了。</P><H4 14pt 0cm 14.5pt"><FONT size=5>cli和sti</FONT></H4><P 0cm 0cm 0pt">正如在第<FONT face="Times New Roman">6</FONT>章中解释过的,<B normal"><FONT face="Times New Roman">cli</FONT></B>和<B normal"><FONT face="Times New Roman">sti</FONT></B>分别用于禁止和启用中断。对于<FONT face="Times New Roman">UP</FONT>这简化为单个<B normal"><FONT face="Times New Roman">cli</FONT></B>或<B normal"><FONT face="Times New Roman">sti</FONT></B>指令。而在<FONT face="Times New Roman">SMP</FONT>情况下,这就很不够了,我们不仅需要禁止本地<FONT face="Times New Roman">CPU</FONT>还要暂时避免其它<FONT face="Times New Roman">CPU</FONT>处理<FONT face="Times New Roman">IRQ</FONT>。因此对于<FONT face="Times New Roman">SMP</FONT>,宏就变成了对<B normal"><FONT face="Times New Roman">__global_cli</FONT></B>和<B normal"><FONT face="Times New Roman">__global_sti</FONT></B>函数的调用。</P><P 12pt 0cm 3.2pt"><b>__global_cli</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1220</FONT>:<FONT face="Times New Roman"> </FONT>把<FONT face="Times New Roman">CPU</FONT>的<FONT face="Times New Roman">EFLAGS</FONT>寄存器复制到本地变量<B normal"><FONT face="Times New Roman">flags</FONT></B>里。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1221</FONT>:<FONT face="Times New Roman"> x86</FONT>系统里的中断使能标志在<FONT face="Times New Roman">EFLAGS</FONT>寄存器的第<FONT face="Times New Roman">9</FONT>位——在第<FONT face="Times New Roman">1205</FONT>行解释了<FONT face="Times New Roman">EFLAG_IF_SHIFT</FONT>的定义。它被用来检测是否已经禁止了中断,这样就不再需要去禁止它们了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1223</FONT>:<FONT face="Times New Roman"> </FONT>禁止这个<FONT face="Times New Roman">CPU</FONT>的中断。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1224</FONT>:<FONT face="Times New Roman"> </FONT>如果该<FONT face="Times New Roman">CPU</FONT>没有正在对<FONT face="Times New Roman">IRQ</FONT>进行处理,<B normal"><FONT face="Times New Roman">__global_cli</FONT></B>就调用<B normal"><FONT face="Times New Roman">get_irqlock</FONT></B>(<FONT face="Times New Roman">1184</FONT>行)来获得全局<FONT face="Times New Roman">IRQ</FONT>锁。如果<FONT face="Times New Roman">CPU</FONT>已经在对<FONT face="Times New Roman">IRQ</FONT>进行处理了,那么正如我们马上要看到的,它已经拥有了该全局<FONT face="Times New Roman">IRQ</FONT>锁。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>现在本<FONT face="Times New Roman">CPU</FONT>已经禁止了中断,而且它也拥有了全局<FONT face="Times New Roman">IRQ</FONT>锁,这样任务就完成了。</P><P 12pt 0cm 3.2pt"><b>__global_sti</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1233</FONT>:<FONT face="Times New Roman"> </FONT>如果<FONT face="Times New Roman">CPU</FONT>没有正在对<FONT face="Times New Roman">IRQ</FONT>进行处理,<B normal"><FONT face="Times New Roman">__global_sti</FONT></B>就在<B normal"><FONT face="Times New Roman">__global_cli</FONT></B>中通过<B normal"><FONT face="Times New Roman">release_irqlock</FONT></B>(<FONT face="Times New Roman">10752</FONT>行)调用来实现对全局<FONT face="Times New Roman">IRQ</FONT>锁的释放工作。如果<FONT face="Times New Roman">CPU</FONT>已经在对<FONT face="Times New Roman">IRQ</FONT>进行处理了,那么它已经拥有了该全局<FONT face="Times New Roman">IRQ</FONT>锁,正如在接下来的部分中将要解释的那样,这个锁将在其它地方被释放掉。<FONT face="Times New Roman"> </FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1235</FONT>:<FONT face="Times New Roman"> </FONT>再次允许在本<FONT face="Times New Roman">CPU</FONT>上进行中断。</P><H4 14pt 0cm 14.5pt"><FONT size=5>irq_enter和irq_exit</FONT></H4><P 0cm 0cm 0pt">第<FONT face="Times New Roman">6</FONT>章中顺便提及了这两个函数的<FONT face="Times New Roman">UP</FONT>版本。包含在一对<B normal"><FONT face="Times New Roman">irq_enter/irq_exit</FONT></B>之中的代码段都是原子操作,这不仅对于其它这样的代码区域是原子的,而且对于<B normal"><FONT face="Times New Roman">cli/sti</FONT></B>宏对来说也是如此。</P><P 12pt 0cm 3.2pt"><b>irq_enter</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1794</FONT>:<FONT face="Times New Roman"> </FONT>调用<B normal"><FONT face="Times New Roman">hardirq_enter</FONT></B>(<FONT face="Times New Roman">10761</FONT>行)自动为本<FONT face="Times New Roman">CPU</FONT>增加全局<FONT face="Times New Roman">IRQ</FONT>计数和本地<FONT face="Times New Roman">IRQ</FONT>计数。这个函数记录了<FONT face="Times New Roman">CPU</FONT>正在处理一个<FONT face="Times New Roman">IRQ</FONT>的情况。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1795</FONT>:<FONT face="Times New Roman"> </FONT>执行循环直到这个<FONT face="Times New Roman">CPU</FONT>得到全局<FONT face="Times New Roman">IRQ</FONT>锁为止。这就是为什么我要在前面说明如果<FONT face="Times New Roman">CPU</FONT>正在处理<FONT face="Times New Roman">IRQ</FONT>,那么它就已经获得了全局<FONT face="Times New Roman">IRQ</FONT>锁的原因:到这个函数退出时,这两个特性都将被加强。对于内核代码来说,把这两个特性分离出去并没有太大的意义——它可以直接调用<B normal"><FONT face="Times New Roman">hardirq_enter</FONT></B>,而且也不用去争夺全局<FONT face="Times New Roman">IRQ</FONT>锁。函数只是没有这样作而已。</P><P 12pt 0cm 3.2pt"><b>irq_exit</b></P>1802: 这个函数转向<B normal">hardirq_enter</B>的相反函数<B normal">hardirq_exit</B>(10767行)。顺便要提及的是,对<B normal">irq_enter</B>和<B normal">irq_exit</B>来说其<B normal">irq</B>参数都被忽略了——至少在x86平台上如此。
<H1 17pt 0cm 16.5pt; TEXT-ALIGN: center" align=center>第<FONT face="Times New Roman">10</FONT>章<FONT face="Times New Roman"> </FONT>对称多处理(<FONT face="Times New Roman">SMP</FONT>)</H1>< 0cm 0cm 0pt">在全书的讨论过程中,我一直在忽略<FONT face="Times New Roman">SMP</FONT>代码,而倾向于把注意力集中在只涉及一个处理器的相对简单的情况。现在已经到了重新访问读者已经熟悉的一些内容的时候了,不过要从一个新的角度来审视它:当内核必须支持多于一个<FONT face="Times New Roman">CPU</FONT>的机器时将发生什么?</P>< 0cm 0cm 0pt">在一般情况下,使用多于一个<FONT face="Times New Roman">CPU</FONT>来完成工作被称为并行处理(<FONT face="Times New Roman">parallel processing</FONT>),它可以被想象成是一段频谱范围,分布式计算(<FONT face="Times New Roman">distributed computing</FONT>)在其中一端,而对称多处理(<FONT face="Times New Roman">SMP</FONT>—<FONT face="Times New Roman">symmetric multiprocessing</FONT>)在另一端。通常,当你沿着该频谱从分布式计算向<FONT face="Times New Roman">SMP</FONT>移动时,系统将变得更加紧密耦合——在<FONT face="Times New Roman">CPU</FONT>之间共享更多的资源——而且更加均匀。在一个典型的分布式系统中,每个<FONT face="Times New Roman">CPU</FONT>通常都至少拥有它自己的高速缓存和<FONT face="Times New Roman">RAM</FONT>。每个<FONT face="Times New Roman">CPU</FONT>还往往拥有自己的磁盘、图形子系统、声卡,监视器等等。</P>< 0cm 0cm 0pt">在极端的情形下,分布式系统经常不外乎就是一组普通的计算机,虽然它们可能具有完全不同的体系结构,但是都共同工作在某个网络之上——它们甚至不需要在同一个<FONT face="Times New Roman">LAN</FONT>里。读者可能知道的一些有趣的分布式系统包括:<FONT face="Times New Roman">Beowulf</FONT>,它是对相当传统而又极其强大的分布式系统的一个通用术语称谓;<FONT face="Times New Roman">SETI@home</FONT>,它通过利用上百万台计算机来协助搜寻地外生命的证据,以及<FONT face="Times New Roman">distributed.net</FONT>,它是类似想法的另一个实现,它主要关注于地球上产生的密码的破解。</P>< 0cm 0cm 0pt"><FONT face="Times New Roman">SMP</FONT>是并行处理的一个特殊情况,系统里所有<FONT face="Times New Roman">CPU </FONT>都是相同的。举例来说,<FONT face="Times New Roman">SMP</FONT>就是你共同支配两块<FONT face="Times New Roman">80486</FONT>或两块<FONT face="Times New Roman">entium</FONT>(具有相同的时钟速率)处理器,而不是一块<FONT face="Times New Roman">80486</FONT>和一块<FONT face="Times New Roman">entium</FONT>,或者一块<FONT face="Times New Roman">entium</FONT>和一块<FONT face="Times New Roman">owerPC</FONT>。在通常的用法中,<FONT face="Times New Roman">SMP</FONT>也意味着所有<FONT face="Times New Roman">CPU</FONT>都是“在相同处境下的”——那就是它们都在同一个计算机里,通过特殊用途的硬件进行彼此通信。</P>< 0cm 0cm 0pt"><FONT face="Times New Roman">SMP</FONT>系统通常是另一种平常的单一(<FONT face="Times New Roman">single</FONT>)计算机——只不过具有两个或更多的<FONT face="Times New Roman">CPU</FONT>。因此,<FONT face="Times New Roman">SMP</FONT>系统除了<FONT face="Times New Roman">CPU</FONT>以外每样东西只有一个——一块图形卡、一个声音卡,等等之类。诸如<FONT face="Times New Roman">RAM</FONT>和磁盘这样以及类似的资源都是为系统的<FONT face="Times New Roman">CPU</FONT>们所共享的。(尽管现在<FONT face="Times New Roman">SMP</FONT>系统中每个<FONT face="Times New Roman">CPU</FONT>都拥有自己的高存缓存的情况已经变得愈发普遍了。)</P>< 0cm 0cm 0pt">分布式配置需要很少的或者甚至不需要来自内核的特殊支持;节点之间的协同是依靠用户空间的应用程序或者诸如网络子系统之类未经修改的内核组件来处理的。但是<FONT face="Times New Roman">SMP</FONT>在计算机系统内创建了一个不同的硬件配置,并由此需要特殊用途的内核支持。比如,内核必须确保<FONT face="Times New Roman">CPU</FONT>在访问它们的共享资源时要相互合作——这是一个读者在<FONT face="Times New Roman">UP</FONT>世界中所不曾遇到的问题。</P>< 0cm 0cm 0pt"><FONT face="Times New Roman">SMP</FONT>的逐渐普及主要是因为通过<FONT face="Times New Roman">SMP</FONT>所获得的性能的提高要比购买几台独立的机器再把它们组合在一起更加便宜和简单,而且还因为它与等待下一代<FONT face="Times New Roman">CPU</FONT>面世相比要快的多。</P>< 0cm 0cm 0pt">非对称多<FONT face="Times New Roman">CPU</FONT>的配置没有受到广泛支持,这是因为对称配置情况所需的硬件和软件支持通常较为简单。不过,内核代码中平台无关的部分实际上并不特别关心是否<FONT face="Times New Roman">CPU</FONT>是相同的——即,是否配置是真正对称的——尽管它也没有进行任何特殊处理以支持非对称配置。例如,在非对称多处理系统中,调度程序应该更愿意在较快的而不是较慢的<FONT face="Times New Roman">CPU</FONT>上运行进程,但是<FONT face="Times New Roman">Linux</FONT>内核没有对此进行区别。</P>< 0cm 0cm 0pt">谚语说得好,“天下没有白吃的午餐”。对于<FONT face="Times New Roman">SMP</FONT>,为提高的性能所付出的代价就是内核复杂度的增加和协同开销的增加。<FONT face="Times New Roman">CPU</FONT>必须安排不互相干涉彼此的工作,但是它们又不能在这种协同上花费太多时间以至于它们显著地耗费额外的<FONT face="Times New Roman">CPU</FONT>能力。</P>< 0cm 0cm 0pt">代码的<FONT face="Times New Roman">SMP</FONT>特定部分由于<FONT face="Times New Roman">UP</FONT>机器存在的缘故而被单独编译,所以仅仅因为有了<FONT face="Times New Roman">SMP</FONT>寄存器是不会使<FONT face="Times New Roman">UP</FONT>寄存器慢下来的。这满足两条久经考验的原理:“为普遍情况进行优化”(<FONT face="Times New Roman">UP</FONT>机器远比<FONT face="Times New Roman">SMP</FONT>机器普遍的多)以及“不为用不着的东西花钱”。</P><H2 13pt 0cm">并行程序设计概念及其原语</H2>< 0cm 0cm 0pt"><FONT face="Times New Roman"> </FONT>具有两个<FONT face="Times New Roman">CPU</FONT>的<FONT face="Times New Roman">SMP</FONT>配置可能是最简单的并行配置,但就算是这最简单的配置也揭开了未知问题的新领域——即使要两块相同的<FONT face="Times New Roman">CPU</FONT>在一起协调的工作,时常也都像赶着猫去放牧一样困难。幸运的是,至少<FONT face="Times New Roman">30</FONT>年前以来,就在这个项目上作了大量和非常熟悉的研究工作。(考虑到第一台电子数字计算机也只是在<FONT face="Times New Roman">50</FONT>年前建造的,那这就是一段令人惊讶的相当长的时间了。)在分析对<FONT face="Times New Roman">SMP</FONT>的支持是如何影响内核代码之前,对该支持所基于的若干理论性概念进行一番浏览将能够极大的简化这个问题。</P>< 0cm 0cm 0pt">注意:并非所有这些信息都是针对<FONT face="Times New Roman">SMP</FONT>内核的。一些要讨论的问题甚至是由<FONT face="Times New Roman">UP</FONT>内核上的并行程序设计所引起的,既要支持中断也要处理进程之间的交互。因此即使你对<FONT face="Times New Roman">SMP</FONT>问题没有特别的兴趣,这部分的讨论也值得一看。</P><H3 13pt 0cm"><FONT size=5>原子操作</FONT></H3>< 0cm 0cm 0pt">在一个并行的环境里,某些动作必须以一种基本的原子方式(<FONT face="Times New Roman">atomically</FONT>)执行——即不可中断。这种操作必须是不可分割的,就象是原子曾经被认为的那样。</P>< 0cm 0cm 0pt">作为一个例子,考虑一下引用计数。如果你想要释放你所控制的一份共享资源并要了解是否还有其它(进程)仍在使用它,你就会减少对该共享资源的计数值并把该值与<FONT face="Times New Roman">0</FONT>进行对照测试。一个典型的动作顺序可能如下开始:</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l6 level1 lfo1; tab-stops: list 42.25pt"><FONT face="Times New Roman">1. CPU</FONT>把当前计数值(假设是<FONT face="Times New Roman">2</FONT>)装载进它的一个寄存器里。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l6 level1 lfo1; tab-stops: list 42.25pt"><FONT face="Times New Roman">2. CPU</FONT>在它的寄存器里把这个值递减;现在它是<FONT face="Times New Roman">1</FONT>。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l6 level1 lfo1; tab-stops: list 42.25pt"><FONT face="Times New Roman">3. CPU</FONT>把新值(<FONT face="Times New Roman">1</FONT>)写回内存里。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l6 level1 lfo1; tab-stops: list 42.25pt"><FONT face="Times New Roman">4. CPU</FONT>推断出:因为该值是<FONT face="Times New Roman">1</FONT>,某个其它进程仍在使用着共享对象,所以它将不会释放该对象。</P>< 0cm 0cm 0pt">对于<FONT face="Times New Roman">UP</FONT>,应不必在此考虑过多(除了某些情况)。但是对于<FONT face="Times New Roman">SMP</FONT>就是另一番景象了:如果另一个<FONT face="Times New Roman">CPU</FONT>碰巧同时也在作同样的事情应如何处理呢?最坏的情形可能是这样的:</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">1. CPU A</FONT>把当前计数值(<FONT face="Times New Roman">2</FONT>)装载进它的一个寄存器里。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">2. CPU B</FONT>把当前计数值(<FONT face="Times New Roman">2</FONT>)装载进它的一个寄存器里。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">3. CPU A</FONT>在它的寄存器里把这个值递减;现在它是<FONT face="Times New Roman">1</FONT>。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">4. CPU B</FONT>在它的寄存器里把这个值递减;现在它是<FONT face="Times New Roman">1</FONT>。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">5. CPU A</FONT>把新值(<FONT face="Times New Roman">1</FONT>)写回内存里。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">6. CPU B</FONT>把新值(<FONT face="Times New Roman">1</FONT>)写回内存里。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">7. CPU A</FONT>推断出:因为该值是<FONT face="Times New Roman">1</FONT>,某个其它进程仍在使用着共享对象,所以它将不会释放该对象。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">8. CPU B</FONT>推断出:因为该值是<FONT face="Times New Roman">1</FONT>,某个其它进程仍在使用着共享对象,所以它将不会释放该对象。</P><P 0cm 0cm 0pt">内存里的引用计数值现在应该是<FONT face="Times New Roman">0</FONT>,然而它却是<FONT face="Times New Roman">1</FONT>。两个进程都去掉了它们对该共享对象的引用,但是没有一个能够释放它。</P><P 0cm 0cm 0pt">这是一个有趣的失败,因为每个<FONT face="Times New Roman">CPU</FONT>都作了它所应该做的事情,尽管这样错误的结果还是发生了。当然这个问题就在于<FONT face="Times New Roman">CPU</FONT>没有协调它们的动作行为——右手不知道左手正在干什么。</P><P 0cm 0cm 0pt">你会怎样试图在软件中解决这个问题呢?从任何一个<FONT face="Times New Roman">CPU</FONT>的观点来看待它——比如说是<FONT face="Times New Roman">CPU A</FONT>。需要通知<FONT face="Times New Roman">CPU B</FONT>它不应使用引用计数值,由于你想要递减该值,所以不管怎样你最好改变某些<FONT face="Times New Roman">CPU B</FONT>所能见到的信息——也就是更新共享内存位置。举例来说,你可以为此目的而开辟出某个内存位置,并且对此达成一致:若任何一个<FONT face="Times New Roman">CPU</FONT>正试图减少引用计数它就包含一个<FONT face="Times New Roman">1</FONT>,如果不是它就为<FONT face="Times New Roman">0</FONT>。使用方法如下:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">1. CPU A</FONT>从特殊内存位置出取出该值把它装载进它的一个寄存器里。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">2. CPU A</FONT>检查它的寄存器里的值并发现它是<FONT face="Times New Roman">0</FONT>(如果不是,它再次尝试,重复直到该寄存器为<FONT face="Times New Roman">0</FONT>为止。)</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">3. CPU A</FONT>把一个<FONT face="Times New Roman">1</FONT>写回特殊内存位置。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">4. CPU A</FONT>访问受保护的引用计数值。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">5. CPU A</FONT>把一个<FONT face="Times New Roman">0</FONT>写回特殊内存位置。</P><P 0cm 0cm 0pt">糟糕,令人不安的熟悉情况又出现了。以下所发生的问题仍然无法避免:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">1. CPU A</FONT>从特殊内存位置出取出该值把它装载进它的一个寄存器里。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">2. CPU B</FONT>从特殊内存位置出取出该值把它装载进它的一个寄存器里。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">3. CPU A</FONT>检查它的寄存器里的值并发现它是<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">4. CPU B</FONT>检查它的寄存器里的值并发现它是<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">5. CPU A</FONT>把一个<FONT face="Times New Roman">1</FONT>写回特殊内存位置。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">6. CPU B</FONT>把一个<FONT face="Times New Roman">1</FONT>写回特殊内存位置。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">7. CPU A</FONT>访问受保护的引用计数值。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">8. CPU B</FONT>访问受保护的引用计数值。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">9. CPU A</FONT>把一个<FONT face="Times New Roman">0</FONT>写回特殊内存位置。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">10. CPU B</FONT>把一个<FONT face="Times New Roman">0</FONT>写回特殊内存位置。</P><P 0cm 0cm 0pt">好吧,或许可以再使用一个特殊内存位置来保护被期望保护初始内存位置的那个特殊内存位置……。</P><P 0cm 0cm 0pt">面对这一点吧:我们在劫难逃。这种方案只会使问题向后再退一层,而不可能解决它。最后,原子性不可能由软件单独保证——必须要有硬件的特殊帮助。</P><P 0cm 0cm 0pt">在<FONT face="Times New Roman">x86</FONT>平台上,<B normal"><FONT face="Times New Roman">lock</FONT></B>指令正好能够提供这种帮助。(准确地说,<B normal"><FONT face="Times New Roman">lock</FONT></B>是一个前缀而非一个单独的指令,不过这种区别和我们的目的没有利害关系。)<B normal"><FONT face="Times New Roman">lock</FONT></B>指令用于在随后的指令执行期间锁住内存总线——至少是对目的内存地址。因为<FONT face="Times New Roman">x86</FONT>可以在内存里直接减值,而无需明确的先把它读入一个寄存器中,这样对于执行一个减值原子操作来说就是万事俱备了:<B normal"><FONT face="Times New Roman">lock</FONT></B>内存总线然后立刻对该内存位置执行<B normal"><FONT face="Times New Roman">decl</FONT></B>操作。</P><P 0cm 0cm 0pt">函数<B normal"><FONT face="Times New Roman">atomic_dec</FONT></B>(<FONT face="Times New Roman">10241</FONT>行)正好为<FONT face="Times New Roman">x86</FONT>平台完成这样的工作。<B normal"><FONT face="Times New Roman">LOCK</FONT></B>宏的<B normal"><FONT face="Times New Roman">SMP</FONT></B>版本在第<FONT face="Times New Roman">10192</FONT>行定义并扩展成<B normal"><FONT face="Times New Roman">lock</FONT></B>指令。(在随后的两行定义的<FONT face="Times New Roman">UP</FONT>版本完全就是空的——单<FONT face="Times New Roman">CPU</FONT>不需要保护自己以防其它<FONT face="Times New Roman">CPU</FONT>的干扰,所以锁住内存总线将完全是在浪费时间。)通过把<B normal"><FONT face="Times New Roman">LOCK</FONT></B>宏放在内嵌编译指令的前边,随后的指令就会为<FONT face="Times New Roman">SMP</FONT>内核而被锁定。如果<FONT face="Times New Roman">CPU B</FONT>在<FONT face="Times New Roman">CPU A</FONT>发挥作用时执行了<B normal"><FONT face="Times New Roman">atomic_dec</FONT></B>函数,那么<FONT face="Times New Roman">CPU B</FONT>就会自动的等待<FONT face="Times New Roman">CPU A</FONT>把锁移开。这样就能够成功了!</P><P 0cm 0cm 0pt">这样还只能说是差不多。最初的问题仍然没有被很好的解决。目标不仅是要自动递减引用计数值,而且还要知道结果值是否是<FONT face="Times New Roman">0</FONT>。现在可以完成原子递减了,可是如果另一个处理器在递减和结果测试之间又“偷偷的”进行了干预,那又怎么办呢?</P><P 0cm 0cm 0pt">幸运的是,解决这个部分问题不需要来自<FONT face="Times New Roman">CPU</FONT>的特殊目的的帮助。不管加锁还是未锁,<FONT face="Times New Roman">x86</FONT>的<B normal"><FONT face="Times New Roman">decl</FONT></B>指令总是会在结果为<FONT face="Times New Roman">0</FONT>时设置<FONT face="Times New Roman">CPU</FONT>的<FONT face="Times New Roman">Zero</FONT>标志位,而且这个标志位是<FONT face="Times New Roman">CPU</FONT>私有的,所以其它<FONT face="Times New Roman">CPU</FONT>的所为是不可能在递减步骤和测试步骤之间影响到这个标志位的。相应的,<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>(<FONT face="Times New Roman">10249</FONT>行)如前完成一次加锁的递减,接着依据<FONT face="Times New Roman">CPU</FONT>的<FONT face="Times New Roman">Zero</FONT>标志位来设置本地变量<FONT face="Times New Roman">c</FONT>。如果递减之后结果是<FONT face="Times New Roman">0</FONT>函数就返回非零值(真)。</P><P 0cm 0cm 0pt">如同其它定义在一个文件里的函数一样,<B normal"><FONT face="Times New Roman">atomic_dec</FONT></B>和<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>都对一个类型为<B normal"><FONT face="Times New Roman">atomic_t</FONT></B>的(<FONT face="Times New Roman">10205</FONT>行)对象进行操作。就像<B normal"><FONT face="Times New Roman">LOCK</FONT></B>,<B normal"><FONT face="Times New Roman">atomic_t</FONT></B>对于<FONT face="Times New Roman">UP</FONT>和<FONT face="Times New Roman">SMP</FONT>也有不同的定义方式——不同之处在于<FONT face="Times New Roman">SMP</FONT>情况里引入了<B normal"><FONT face="Times New Roman">volatile</FONT></B>限定词,它指示<FONT face="Times New Roman">gcc</FONT>不要对被标记的变量做某种假定(比如,不要假定它可以被安全的保存在一个寄存器里)。</P><P 0cm 0cm 0pt">顺便提及一下,读者在这段代码里看到的垃圾代码<FONT face="Times New Roman">­­­­­<B normal">__atomic_fool_gcc</B></FONT>据报告已不再需要了;它曾用于纠正在<FONT face="Times New Roman">gcc</FONT>的早期版本下代码生成里的一个故障。</P><H3 13pt 0cm"><FONT face="Times New Roman" size=5>Test-And-Set</FONT></H3><P 0cm 0cm 0pt">经典的并行原语是<FONT face="Times New Roman">test-and-set</FONT>。<FONT face="Times New Roman">test-and-set</FONT>操作自动地从一个内存位置读取一个值然后写入一个新值,并把旧值返回。典型的,该位置可以保存<FONT face="Times New Roman">0</FONT>或者<FONT face="Times New Roman">1</FONT>,而且<FONT face="Times New Roman">test-and-set</FONT>所写的新值是<FONT face="Times New Roman">1</FONT>——因此是“设置(<FONT face="Times New Roman">set</FONT>)”。与<FONT face="Times New Roman">test-and-set</FONT>对等的是<FONT face="Times New Roman">test-and-clear</FONT>,它是同样的操作除了写入的是<FONT face="Times New Roman">0</FONT>而不是<FONT face="Times New Roman">1</FONT>。一些<FONT face="Times New Roman">test-and-set</FONT>的变体既能写入<FONT face="Times New Roman">1</FONT>也可以写入<FONT face="Times New Roman">0</FONT>,因此<FONT face="Times New Roman">test-and-set</FONT>和<FONT face="Times New Roman">test-and-clear</FONT>就能够成为一体,只是操作数不同而已。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">test-and-set</FONT>原语足以实现任何其它并行安全的操作。(实际上,在某些<FONT face="Times New Roman">CPU</FONT>上<FONT face="Times New Roman">test-and-set</FONT>是唯一被提供的此类原语。)比如,原本<FONT face="Times New Roman">test-and-set</FONT>是能够用于前边的例子之中来保护引用计数值的。相似的方法以被尝试——从一个内存位置读取一个值,检查它是否为<FONT face="Times New Roman">0</FONT>,如果是则写入一个<FONT face="Times New Roman">1</FONT>,然后继续访问受保护的值。这种尝试的失败并不是因为它在逻辑上是不健全的,而是因为没有可行的方法使其自动完成。假使有了一个原子的<FONT face="Times New Roman">test-and-set</FONT>,你就可以不通过使用<B normal"><FONT face="Times New Roman">lock</FONT></B>来原子化<B normal"><FONT face="Times New Roman">decl</FONT></B>的方法而顺利通过了。</P><P 0cm 0cm 0pt">然而,<FONT face="Times New Roman">test-and-set</FONT>也有缺点:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo5; tab-stops: list 42.25pt">l 它是一个低级的原语——在所有与它打交道时,其它原语都必须在它之上被执行。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo5; tab-stops: list 42.25pt">l 它并不经济——当机器测试该值并发现它已经是<FONT face="Times New Roman">1</FONT>了怎么办呢?这个值在内存里不会被搞乱,因为只要用同样的值复写它即可。可事实是它已被设置就意味着其它进程正在访问受保护的对象,所以还不能这样执行。额外需要的逻辑——测试并循环——会浪费<FONT face="Times New Roman">CPU</FONT>时钟周期并使得程序变得更大一些(它还会浪费高速缓存里的空间)。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">x86</FONT>的<B normal"><FONT face="Times New Roman">lock</FONT></B>指令使高级指令更容易执行,但是你也可以在上执行原子<FONT face="Times New Roman">test-and-set</FONT>操作。最直接的方式是把<B normal"><FONT face="Times New Roman">lock</FONT></B>和<B normal"><FONT face="Times New Roman">btsl</FONT></B>指令(位<FONT face="Times New Roman">test-and-set</FONT>)联合起来使用。这种方法要被本章后边介绍的自旋锁(<FONT face="Times New Roman">spinlock</FONT>)所用到。</P><P 0cm 0cm 0pt">另一种在<FONT face="Times New Roman">x86</FONT>上实现的方法是用它的<B normal"><FONT face="Times New Roman">xchg</FONT></B>(<FONT face="Times New Roman">exchange</FONT>)指令,它能够被<FONT face="Times New Roman">x86</FONT>自动处理,就好像它的前面有一个<B normal"><FONT face="Times New Roman">lock</FONT></B>指令一样——只要它的一个操作数是在内存里。<B normal"><FONT face="Times New Roman">xchg</FONT></B>要比<B normal"><FONT face="Times New Roman">lock/ btsl</FONT></B>组合更为普遍,因为它可以一次交换<FONT face="Times New Roman">8</FONT>、<FONT face="Times New Roman">16</FONT>,或者<FONT face="Times New Roman">32</FONT>位而不仅仅是<FONT face="Times New Roman">1</FONT>位。除了一个在<FONT face="Times New Roman">arch/i386/kernel/entry.S</FONT>里的使用之外,内核对<B normal"><FONT face="Times New Roman">xchg</FONT></B>指令的使用都隐藏在<B normal"><FONT face="Times New Roman">xchg</FONT></B>宏(<FONT face="Times New Roman">13052</FONT>行)之后,而它又是在函数<FONT face="Times New Roman">__<B normal">xchg</B></FONT>(<FONT face="Times New Roman">13061</FONT>行)之上实现的。这样是便于在平台相关的代码里内核代码也可以使用<B normal"><FONT face="Times New Roman">xchg</FONT></B>宏;每种平台都提供它自己对于该宏的等价的实现。</P><P 0cm 0cm 0pt">有趣的时,<B normal"><FONT face="Times New Roman">xchg</FONT></B>宏是另一个宏,<B normal"><FONT face="Times New Roman">tas</FONT></B>(<FONT face="Times New Roman">test-and-set</FONT>——<FONT face="Times New Roman">13054</FONT>行)的基础。然而,内核代码的任何一个地方都没有用到这个宏。</P><P 0cm 0cm 0pt">内核有时候使用<B normal"><FONT face="Times New Roman">xchg</FONT></B>宏来完成简单的<FONT face="Times New Roman">test-and-set</FONT>操作(尽管不必在锁变得可用之前一直循环,如同第<FONT face="Times New Roman">22770</FONT>行),并把它用于其它目的(如同第<FONT face="Times New Roman">27427</FONT>行)。</P><H3 13pt 0cm"><FONT size=5>信号量</FONT></H3><P 0cm 0cm 0pt">第<FONT face="Times New Roman">9</FONT>章中讨论了信号量的基本概念并演示了它们在进程间通信中的用法。内核为达自己的目的有其特有的信号量实现,它们被特别的称为是“内核信号量”。(在这一章里,未经修饰的名词“信号量”应被理解为是“内核信号量”。)第<FONT face="Times New Roman">9</FONT>章里所讨论的基本信号量的概念同样适用于内核信号量:允许一个可访问某资源用户的最大数目(最初悬挂在吊钩上钥匙的特定数目),然后规定每个申请资源者都必须先获得一把钥匙才能使用该资源。</P><P 0cm 0cm 0pt">到目前为止,你大概应该已经发现信号量如何能够被建立在<FONT face="Times New Roman">test-and-set</FONT>之上并成为二元(“唯一钥匙”)信号量,或者在像<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>这样的函数之上成为计数信号量的过程。内核正好就完成着这样的工作:它用整数代表信号量并使用函数<B normal"><FONT face="Times New Roman">down</FONT></B>(<FONT face="Times New Roman">11644</FONT>行)和<B normal"><FONT face="Times New Roman">up</FONT></B>(<FONT face="Times New Roman">11714</FONT>行)以及其它一些函数来递减和递增该整数。读者将看到,用于减少和增加整数的底层代码和<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>及其它类似函数所使用的代码是一样的。</P><P 0cm 0cm 0pt">作为相关历史事件的提示,第一位规范信号量概念的研究者,<FONT face="Times New Roman">Edsger Dijistra</FONT>是荷兰人,所以信号量的基础操作就用荷兰语命名为:<FONT face="Times New Roman">Proberen</FONT>和<FONT face="Times New Roman">Verhogen</FONT>,常缩写成<FONT face="Times New Roman">P</FONT>和<FONT face="Times New Roman">V</FONT>。这对术语被翻译成“测试(<FONT face="Times New Roman">test</FONT>)”(检查是否还有一把钥匙可用,若是就取走)和“递增(<FONT face="Times New Roman">increment</FONT>)”(把一个钥匙放回到吊钩之上)。那些词首字母正是在前一章中所引入的术语“获得(<FONT face="Times New Roman">procure</FONT>)”和“交出(<FONT face="Times New Roman">vacate</FONT>)”的来源。<FONT face="Times New Roman">Linux</FONT>内核打破了这个传统,用操作<B normal"><FONT face="Times New Roman">down</FONT></B>和<B normal"><FONT face="Times New Roman">up</FONT></B>的称呼取代了它们。</P><P 0cm 0cm 0pt">内核用一个非常简单的类型来代表信号量:定义在<FONT face="Times New Roman">11609</FONT>行的<B normal"><FONT face="Times New Roman">struct semaphore</FONT></B>。他只有三个成员:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo6; tab-stops: list 42.25pt">l <B normal"><FONT face="Times New Roman">count</FONT></B>——跟踪仍然可用的钥匙数目。如果是<FONT face="Times New Roman">0</FONT>,钥匙就被取完了;如果是负数,钥匙被取完而且还有其它申请者在等待它。另外,如果<B normal"><FONT face="Times New Roman">count</FONT></B>是<FONT face="Times New Roman">0</FONT>或负数,那么其它申请者的数目就等于<B normal"><FONT face="Times New Roman">count</FONT></B>的绝对值。<B normal"><p></p></B></P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm"><B normal"><FONT face="Times New Roman">Sema_init</FONT></B>宏(<FONT face="Times New Roman">11637</FONT>行)允许<B normal"><FONT face="Times New Roman">count</FONT></B>被初始化为任何值,所以内核信号量可以是二元的(初始化<B normal"><FONT face="Times New Roman">count</FONT></B>为<FONT face="Times New Roman">1</FONT>)也可以是计数型的(赋予它某个更大的初始值)。所有内核信号量代码都完全支持二元和计数型信号量,前者可作为后者的一个特例。不过在实践中<B normal"><FONT face="Times New Roman">count</FONT></B>总是被初始化为<FONT face="Times New Roman">1</FONT>,这样内核信号量也总是二元类型的。尽管如此,没有什么能够阻止一个开发者将来增加一个新的计数信号量。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm">要顺便提及的是,把<B normal"><FONT face="Times New Roman">count</FONT></B>初始化为正值而且用递减它来表明你需要一个信号量的方法并没有什么神秘之处。你也可以用一个负值(或者是<FONT face="Times New Roman">0</FONT>)来初始化计数值然后增加它,或者遵循其它的方案。使用正的数字只是内核所采用的办法,而这碰巧和我们头脑中的吊钩上的钥匙模型吻合得相当好。的确,正如你将看到的那样,内核锁采用的是另一种方式工作——它被初始化为负值,并在进程需要它时进行增加。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo6; tab-stops: list 42.25pt">l <B normal"><FONT face="Times New Roman">waking</FONT></B>——在<B normal"><FONT face="Times New Roman">up</FONT></B>操作期间及之后被暂时使用;如果<B normal"><FONT face="Times New Roman">up</FONT></B>正在释放信号量则它被设置为<FONT face="Times New Roman">1</FONT>,否则是<FONT face="Times New Roman">0</FONT>。<B normal"><p></p></B></P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo6; tab-stops: list 42.25pt">l <B normal"><FONT face="Times New Roman">wait</FONT></B>——因为要等待这个信号量再次变为可用而不得不被挂起的进程队列。</P><P 12pt 0cm 3.2pt"><b>down</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11644</FONT>:<B normal"><FONT face="Times New Roman">down</FONT></B>操作递减信号量计数值。你可能会认为它与概念里的实现一样简单,不过实际上远不是这样简单。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11648</FONT>:减少信号量计数值——要确保对<FONT face="Times New Roman">SMP</FONT>这是自动完成的。对于<FONT face="Times New Roman">SMP</FONT>来说(当然也适于<FONT face="Times New Roman">UP</FONT>),除了被访问的整数是在一个不同类型的<B normal"><FONT face="Times New Roman">struct</FONT></B>之内以外,这同在<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>中所完成的工作本质上是相同的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>读者可能会怀疑<B normal"><FONT face="Times New Roman">count</FONT></B>是否会下溢。它不会:进程总是在递减<B normal"><FONT face="Times New Roman">count</FONT></B>之后进入休眠,所以一个给定的进程一次只能获得一个信号量,而且<B normal"><FONT face="Times New Roman">int</FONT></B>具有的负值要比进程的数目多的多。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11652</FONT>:如果符号位被设置,信号量就是负值。这意味着甚至它在被递减之前就是<FONT face="Times New Roman">0</FONT>或者负值了,这样进程无法得到该信号量并因此而应该休眠一直到它变成可用。接下来的几行代码十分巧妙地完成了这一点。如果符号位被设置则执行<B normal"><FONT face="Times New Roman">js</FONT></B>跳转(即若<B normal"><FONT face="Times New Roman">decl</FONT></B>的结果是负的它就跳转),<B normal"><FONT face="Times New Roman">2f</FONT></B>标识出跳转的目的地。<B normal"><FONT face="Times New Roman">2f</FONT></B>并非十六进制值——它是特殊的<FONT face="Times New Roman">GNU</FONT>汇编程序语法:<B normal"><FONT face="Times New Roman">2</FONT></B>表示跳转到本地符号“<FONT face="Times New Roman">2</FONT>”,<B normal"><FONT face="Times New Roman">f</FONT></B>表示向前搜索这个符号。(<B normal"><FONT face="Times New Roman">2b</FONT></B>将表示向后搜索最近的本地符号“<FONT face="Times New Roman">2</FONT>”。)这个本地符号在第<FONT face="Times New Roman">11655</FONT>行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11653</FONT>:分支转移没有执行,所以进程得到了信号量。虽然看起来不是这样,但是这实际已经到达<B normal"><FONT face="Times New Roman">down</FONT></B>的末尾。稍后将对此进行解释。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11654</FONT>:<B normal"><FONT face="Times New Roman">down</FONT></B>的技巧在于指令<B normal"><FONT face="Times New Roman">.section</FONT></B>紧跟在跳转目标的前面,它表示把随后的代码汇编到内核的一个单独的段中——该段被称为<B normal"><FONT face="Times New Roman">.text.lock</FONT></B>。这个段将在内存中被分配并标识为可执行的。这一点是由跟在段名之后的<B normal"><FONT face="Times New Roman">ax</FONT></B>标志字符串来指定的——注意这个<B normal"><FONT face="Times New Roman">ax</FONT></B>与<FONT face="Times New Roman">x86</FONT>的<FONT face="Times New Roman">AX</FONT>寄存器无关。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这样的结果是,汇编程序将把<FONT face="Times New Roman">11655</FONT>和<FONT face="Times New Roman">11656</FONT>行的指令从<B normal"><FONT face="Times New Roman">down</FONT></B>所在的段里转移到可执行内核的一个不同的段里。所以这些行生成的目标代码与其前边的行所生成的代码从物理上不是连续的。这就是为什么说<FONT face="Times New Roman">11653</FONT>行是<B normal"><FONT face="Times New Roman">down</FONT></B>的结尾的原因。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11655</FONT>:当信号量无法得到时跳转到的这一目的行。<B normal"><FONT face="Times New Roman">Pushl $1b</FONT></B>并不是要把十六进制值<FONT face="Times New Roman">1b</FONT>压入栈中——如果要执行那种工作应该使用<B normal"><FONT face="Times New Roman">pushl $0x1b</FONT></B>(也可以写成是不带<B normal"><FONT face="Times New Roman">$</FONT></B>的)。正确的解释是,这个<B normal"><FONT face="Times New Roman">1b</FONT></B>和前边见到的<B normal"><FONT face="Times New Roman">2f</FONT></B>一样都是<FONT face="Times New Roman">GNU</FONT>汇编程序语法——它指向一个指令的地址;在此情形中,它是向后搜索时碰到的第一个本地标识“<FONT face="Times New Roman">1</FONT>”的地址。所以,这条指令是把<FONT face="Times New Roman">11653</FONT>行代码的地址压入栈中;这个地址将成为返回地址,以便在随后的跳转操作之后,执行过程还能返回到<B normal"><FONT face="Times New Roman">down</FONT></B>的末尾。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11656</FONT>:开始跳转到<B normal"><FONT face="Times New Roman">__down_failed</FONT></B>(不包括在本书之内)。这个函数在栈里保存几个寄存器并调用后边要介绍的<B normal"><FONT face="Times New Roman">__down</FONT></B>(<FONT face="Times New Roman">26932</FONT>行)来完成等待信号量的工作。一旦<B normal"><FONT face="Times New Roman">__down</FONT></B>返回了,<B normal"><FONT face="Times New Roman">__down_failed</FONT></B>就返回到<B normal"><FONT face="Times New Roman">down</FONT></B>,而它也随之返回。一直到进程获得了信号量<B normal"><FONT face="Times New Roman">__down</FONT></B>才会返回;最终结果就是只要<B normal"><FONT face="Times New Roman">down</FONT></B>返回,进程就得到信号量了,而不管它是立刻还是经过等待后获得的它。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11657</FONT>:伪汇编程序指令<B normal"><FONT face="Times New Roman">.previous</FONT></B>的作用未在正式文档中说明,但是它的意思肯定是还原到以前的段中,结束<FONT face="Times New Roman">11654</FONT>行里的伪指令<B normal"><FONT face="Times New Roman">.section</FONT></B>的作用效果。</P><P 12pt 0cm 3.2pt"><b>down_interruptible</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11664</FONT>:<B normal"><FONT face="Times New Roman">down_interruptible</FONT></B>函数被用于进程想要获得信号量但也愿意在等待它时被信号中断的情况。这个函数与<B normal"><FONT face="Times New Roman">down</FONT></B>的实现非常相似,不过有两个区别将在随后的两段里进行解释。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11666</FONT>:第一个区别是<B normal"><FONT face="Times New Roman">down_interruptible</FONT></B>函数返回一个<B normal"><FONT face="Times New Roman">int</FONT></B>值来指示是否它获得了信号量或者被一个信号所打断。在前一种情况里返回值(在<B normal"><FONT face="Times New Roman">result</FONT></B>里)是<FONT face="Times New Roman">0</FONT>,在后一种情况里它是负值。这部分上是由<FONT face="Times New Roman">11675</FONT>行代码完成的,如果函数未经等待获得了信号量则该行把<B normal"><FONT face="Times New Roman">result</FONT></B>设置为<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11679</FONT>:第二个区别是<B normal"><FONT face="Times New Roman">down_interruptible</FONT></B>函数跳转到<FONT face="Times New Roman">__<B normal">down_failed_interruptible</B></FONT>(不包括在本书之内)而不是<FONT face="Times New Roman">__<B normal">down_failed</B></FONT>。因循<FONT face="Times New Roman">__<B normal">down_failed</B></FONT>建立起来的模式,<FONT face="Times New Roman">__<B normal">down _failed_interruptible</B></FONT>只是调整几个寄存器并调用将在随后进行研究的<FONT face="Times New Roman">__<B normal">down_interruptible</B></FONT>函数(<FONT face="Times New Roman">26942</FONT>行)。要注意的是<FONT face="Times New Roman">11676</FONT>行为<FONT face="Times New Roman">__<B normal">down_failed_ interruptible</B></FONT>设置的返回目标跟在<B normal"><FONT face="Times New Roman">xorl</FONT></B>之后,<B normal"><FONT face="Times New Roman">xorl</FONT></B>用于在信号量可以被立刻获得的情况中把<B normal"><FONT face="Times New Roman">result</FONT></B>归<FONT face="Times New Roman">0</FONT>。<B normal"><FONT face="Times New Roman">down_interruptible</FONT></B>函数的返回值再被复制进<B normal"><FONT face="Times New Roman">result</FONT></B>中。</P><P 12pt 0cm 3.2pt"><b>down_trylock</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11687</FONT>:除了调用<FONT face="Times New Roman">__<B normal">down_failed_trylock</B></FONT>函数(当然还要调用<FONT face="Times New Roman">26961</FONT>行的<FONT face="Times New Roman">__<B normal">down_trylock</B></FONT>函数,我们将在后面对它进行检查)之外,<B normal"><FONT face="Times New Roman">down_trylock</FONT></B>函数和<B normal"><FONT face="Times New Roman">down_interruptible</FONT></B>函数相同。因此,在这里不必对<B normal"><FONT face="Times New Roman">down_trylock</FONT></B>函数进行更多解释。</P><P 12pt 0cm 3.2pt"><b>DOWN_VAR</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26900</FONT>:这是作为<FONT face="Times New Roman">__<B normal">down</B></FONT>和<FONT face="Times New Roman">_<B normal">down_interruptible</B></FONT>共同代码因子的三个宏中的第一个。它只是声明了几个变量。</P><P 12pt 0cm 3.2pt"><b>DOWN_HEAD</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26904</FONT>:这个宏使任务<B normal"><FONT face="Times New Roman">tsk</FONT></B>(被<B normal"><FONT face="Times New Roman">DOWN_VAR</FONT></B>所声明)转移到<B normal"><FONT face="Times New Roman">task_state</FONT></B>给出的状态,然后把<B normal"><FONT face="Times New Roman">tsk</FONT></B>添加到等待信号量的任务队列。最后,它开始一个无限循环,在此循环期间当<B normal"><FONT face="Times New Roman">__down</FONT></B>和<B normal"><FONT face="Times New Roman">__down_interruptible</FONT></B>准备退出时将使用<B normal"><FONT face="Times New Roman">break</FONT></B>语句结束该循环。</P><P 12pt 0cm 3.2pt"><b>DOWN_TAIL</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26926</FONT>:这个宏完成循环收尾工作,把<B normal"><FONT face="Times New Roman">tsk</FONT></B>设置回<B normal"><FONT face="Times New Roman">task_state</FONT></B>的状态,为再次尝试获得信号量做准备。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26929</FONT>:循环已经退出;<B normal"><FONT face="Times New Roman">tsk</FONT></B>已或者得到了信号量或者被一个信号中断了(仅适于<B normal"><FONT face="Times New Roman">__down_ interruptible</FONT></B>)。无论哪一种方式,任务已准备再次运行而不再等待该信号量了,因此它被转移回<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>并从信号量的等待队列里被注销。</P><P 12pt 0cm 3.2pt"><b>__down</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26932</FONT>:<B normal"><FONT face="Times New Roman">__down</FONT></B>和<B normal"><FONT face="Times New Roman">__down_interruptible</FONT></B>遵循以下模式:</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo7; tab-stops: list 57.25pt"><FONT face="Times New Roman">1. </FONT>用<B normal"><FONT face="Times New Roman">DOWN_VAR</FONT></B>声明所需的本地变量,随后可能还有补充的本地变量声明。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo7; tab-stops: list 57.25pt"><FONT face="Times New Roman">2. </FONT>以<B normal"><FONT face="Times New Roman">DOWN_HEAD</FONT></B>开始进入无穷循环。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo7; tab-stops: list 57.25pt"><FONT face="Times New Roman">3. </FONT>在循环体内完成函数特定的(<FONT face="Times New Roman">function-specific</FONT>)工作。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo7; tab-stops: list 57.25pt"><FONT face="Times New Roman">4. </FONT>重新调度。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo7; tab-stops: list 57.25pt"><FONT face="Times New Roman">5. </FONT>以<B normal"><FONT face="Times New Roman">DOWN_TAIL</FONT></B>结束。注意对<B normal"><FONT face="Times New Roman">schedule</FONT></B>的调用(<FONT face="Times New Roman">26686</FONT>行,在第<FONT face="Times New Roman">7</FONT>章里讨论过)可以被移进<B normal"><FONT face="Times New Roman">DOWN_TAIL</FONT></B>宏中。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo7; tab-stops: list 57.25pt"><FONT face="Times New Roman">6. </FONT>完成任何函数特定的收尾工作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">我将只对函数特定的步骤(第<FONT face="Times New Roman">3</FONT>和第<FONT face="Times New Roman">6</FONT>步)进行讨论。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26936</FONT>:<B normal"><FONT face="Times New Roman">__down</FONT></B>的循环体调用<B normal"><FONT face="Times New Roman">waking_non_zero</FONT></B>(未包括),它自动检查<B normal"><FONT face="Times New Roman">sem->waking</FONT></B>来判断是否进程正被<B normal"><FONT face="Times New Roman">up</FONT></B>唤醒。如果是这样,它将<B normal"><FONT face="Times New Roman">waking</FONT></B>归零并返回<FONT face="Times New Roman">1</FONT>(这仍然是同一个原子操作的一部分);如果不是,它返回<FONT face="Times New Roman">0</FONT>。因此,它返回的值指示了是否进程获得了信号量。如果它获得了值,循环就退出,接着函数也将返回。否则,进程将继续等待。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>顺便要说明的是,观察一下<B normal"><FONT face="Times New Roman">__down</FONT></B>尝试获得信号量是在调用<B normal"><FONT face="Times New Roman">schedule</FONT></B>之前。如果信号量的计数值已知为负值时,为什么不用另一种相反的方式来实现它呢?实际上它对于第一遍循环之后的任何一遍重复都是没有影响的,但是去掉一次没有必要的检查可以稍微加快第一遍循环的速度。如果需要为此提出什么特别的理由的话,那可能就是因为自从信号量第一次被检查之后的几个微秒内它就应该可以被释放(可能是在另一个处理器上),而且额外获取标志要比一次额外调度所付出的代价少得多。因此<B normal"><FONT face="Times New Roman">__down</FONT></B>可能还可以在重新调度之前做一次快速检查。</P><P 12pt 0cm 3.2pt"><b>__down_interruptible</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26942</FONT>:<B normal"><FONT face="Times New Roman">__down_interruptible</FONT></B>除了允许被信号中断以外,它和<B normal"><FONT face="Times New Roman">__down</FONT></B>在本质上是一样的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26948</FONT>:所以,当获取信号量时对<B normal"><FONT face="Times New Roman">waking_non_zero_interruptible</FONT></B>(未包括)进行调用。如果它没能得到信号量就返回<FONT face="Times New Roman">0</FONT>,如果得到就返回<FONT face="Times New Roman">1</FONT>,或者如果它被一个信号所中断就返回<B normal"><FONT face="Times New Roman">–EINTR</FONT></B>。在第一种情况下,循环继续。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26958</FONT>:否则,<B normal"><FONT face="Times New Roman">__down_interruptible</FONT></B>退出,如果它得到信号量就返回<FONT face="Times New Roman">0</FONT>(不是<FONT face="Times New Roman">1</FONT>),或者假如被中断则返回<B normal"><FONT face="Times New Roman">–EINTR</FONT></B>。</P><P 12pt 0cm 3.2pt"><b>__down_trylock</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26961</FONT>:有时在不能立刻获得信号量的情况下,内核也需要继续运行。所以,<B normal"><FONT face="Times New Roman">__down_trylock</FONT></B>不在循环之内。它仅仅调用<B normal"><FONT face="Times New Roman">waking_nonzero_trylock</FONT></B>(未包括),该函数夺取信号量,如果失败就递增该信号量的<B normal"><FONT face="Times New Roman">count</FONT></B>(因为内核不打算继续等待下去)然后返回。</P><P 12pt 0cm 3.2pt"><b>up</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11714</FONT>:我们已经详尽的分析了内核尝试获得信号量时的情况,也讨论了它失败时的情况。现在是考察另一面的时候了:当释放一个信号量时将发生什么。这一部分相对简单。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11721</FONT>:原子性地递增信号量的计数值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11722</FONT>:如果结果小于等于<FONT face="Times New Roman">0</FONT>,就有某个进程正在等待被唤醒。<FONT face="Times New Roman">up</FONT>向前跳转到<FONT face="Times New Roman">11725</FONT>行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11724</FONT>:<B normal"><FONT face="Times New Roman">up</FONT></B>采用了<B normal"><FONT face="Times New Roman">down</FONT></B>里同样的技巧:这一行进入了内核的单独的一段,而不是在<B normal"><FONT face="Times New Roman">up</FONT></B>本身的段内。<B normal"><FONT face="Times New Roman">up</FONT></B>的末尾的地址被压入栈然后<B normal"><FONT face="Times New Roman">up</FONT></B>跳转到<B normal"><FONT face="Times New Roman">__up_wakeup</FONT></B>(未包括)。这里完成如同<B normal"><FONT face="Times New Roman">__down_failed</FONT></B>一样的寄存器操作并调用下边要讨论的<B normal"><FONT face="Times New Roman">__up</FONT></B>函数。</P><P 12pt 0cm 3.2pt"><b>__up</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26877</FONT>:<B normal"><FONT face="Times New Roman">__up</FONT></B>函数负责唤醒所有等待该信号量的进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26897</FONT>:调用<B normal"><FONT face="Times New Roman">wake_one_more</FONT></B>(未包括在本书中),该函数检查是否有进程在等待该信号量,如果有,就增加<B normal"><FONT face="Times New Roman">waking</FONT></B>成员来通知它们可以尝试获取它了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26880</FONT>:利用<B normal"><FONT face="Times New Roman">wake_up</FONT></B>宏(<FONT face="Times New Roman">16612</FONT>行),它只是调用<B normal"><FONT face="Times New Roman">__wake_up</FONT></B>函数(<FONT face="Times New Roman">26829</FONT>行)来唤醒所有等待进程。</P><P 12pt 0cm 3.2pt"><b>__wake_up</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26829</FONT>:正如在第<FONT face="Times New Roman">2</FONT>章中所讨论的那样,<B normal"><FONT face="Times New Roman">__wake_up</FONT></B>函数唤醒所有传递给它的在等待队列上的进程,假如它们处于被<B normal"><FONT face="Times New Roman">mode</FONT></B>所隐含的状态之一的话。当从<B normal"><FONT face="Times New Roman">wake_up</FONT></B>被调用时,函数唤醒所有处于<B normal"><FONT face="Times New Roman">TASK_UNINTERRUPTIBLE</FONT></B>或<B normal"><FONT face="Times New Roman">TASK_INTERRUPTIBLE</FONT></B>状态的进程;当从<B normal"><FONT face="Times New Roman">wake_up_interruptible</FONT></B>(<FONT face="Times New Roman">16614</FONT>行)被调用时,它只唤醒处于<B normal"><FONT face="Times New Roman">TASK_INTERRUPTIBLE</FONT></B>状态的任务。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26842</FONT>:进程用<B normal"><FONT face="Times New Roman">wake_up_process</FONT></B>(<FONT face="Times New Roman">26356</FONT>行)被唤醒,该函数曾在以前提到过,它将在本章随后进行详细介绍。</P><P 0cm 0cm 0pt">现在所感兴趣的是唤醒所有进程后的结果。因为<B normal"><FONT face="Times New Roman">__wake_up</FONT></B>唤醒所有队列里的进程,而不仅仅是队列里的第一个,所以它们都要竞争信号量——在<FONT face="Times New Roman">SMP</FONT>里,它们可以精确的同时做这件事。通常,获胜者将首先获得<FONT face="Times New Roman">CPU</FONT>。这个进程将是拥有最大“<FONT face="Times New Roman">goodness</FONT>”的进程(回忆一下第<FONT face="Times New Roman">7</FONT>章中<FONT face="Times New Roman">26338</FONT>行对<B normal"><FONT face="Times New Roman">goodness</FONT></B>的讨论)。<FONT face="Times New Roman"> </FONT>这一点意义非常重大,因为拥有更高优先权的进程应该首先被给予继续其工作的机会。(这对于实时进程尤其重要。)</P><P 0cm 0cm 0pt">这种方案的不足之处是有发生饥饿(<FONT face="Times New Roman">starvation</FONT>)的危险,这发生在一个进程永远不能得到它赖以继续运行的资源时。这里可能会发生饥饿现象:假如两个进程反复竞争同一个信号量,而第一个进程总是有比第二个更高的优先权,那么第二个进程将永远不会得到<FONT face="Times New Roman">CPU</FONT>。这种场景同它应该的运行方式存在一定差距——设想一个是实时进程而另一个以<FONT face="Times New Roman">20</FONT>的<FONT face="Times New Roman">niceness</FONT>运行。我们可以通过只唤醒队列里第一个进程的方法来避免这种饥饿的危险,可是那样又将意味着有时候会耽误从各个方面来说都更有资格的进程对<FONT face="Times New Roman">CPU</FONT>的使用。</P><P 0cm 0cm 0pt">以前对此没有讨论过,可是<FONT face="Times New Roman">Linux</FONT>的调度程序在适当的环境下也能够使得<FONT face="Times New Roman">CPU</FONT>的一个进程被彻底饿死。这不完全是一件坏事——只是一种设计决策而已——而且至少应用于通篇内核代码的原则是一致的,这就很好。还要注意的是使用前边讨论过的其它机制,饥饿现象也同样会发生。例如说,<FONT face="Times New Roman">test-and-set</FONT>原语就是和内核信号量一样的潜在饥饿根源。</P><P 0cm 0cm 0pt">无论如何,在实际中,饥饿是非常少见的——它只是一个有趣的理论案例。</P><H3 13pt 0cm"><FONT face="Times New Roman" size=5>Spinlocks</FONT></H3><P 0cm 0cm 0pt">这一章里最后一个重要的并行程序设计原语是自旋锁(<FONT face="Times New Roman">spinlock</FONT>)。自旋锁的思想就是在一个密封的循环里坚持反复尝试夺取一个资源(一把锁)直到成功为止。这通常是通过在类似<FONT face="Times New Roman">test-and-set</FONT>操作之上进行循环来实现的——即,旋转(<FONT face="Times New Roman">spinning</FONT>)——一直到获得该锁。</P><P 0cm 0cm 0pt">如果这听起来好像是一个二元信号量,那是因为它就是一个二元信号量。自旋锁和二元信号量唯一的概念区别就是你不必循环等待一个信号量——你可以夺取信号量,也可以在不能立刻得到它时放弃申请。因此,自旋锁原本是可以通过在信号量代码外再包裹一层循环来实现的。不过,因为自旋锁是信号量的一个受限特例,它们有更高效的实现方法。</P><P 0cm 0cm 0pt">自旋锁变量——其中的一位被测试和设置——总是<B normal"><FONT face="Times New Roman">spinlock_t</FONT></B>类型(<FONT face="Times New Roman">12785</FONT>行)。只有<B normal"><FONT face="Times New Roman">spinlock_t</FONT></B>的最低位被使用;如果锁可用,则它是<FONT face="Times New Roman">0</FONT>,如果被取走,则它是<FONT face="Times New Roman">1</FONT>。在一个声明里,自旋锁被初始化为值<B normal"><FONT face="Times New Roman">SPIN_LOCK_UNLOCKED</FONT></B>(<FONT face="Times New Roman">12789</FONT>行);它也可以用<B normal"><FONT face="Times New Roman">spin_lock_init</FONT></B>函数(<FONT face="Times New Roman">12791</FONT>行)来初始化。这两者都把<B normal"><FONT face="Times New Roman">spinlock_t</FONT></B>的<B normal"><FONT face="Times New Roman">lock</FONT></B>成员设置成<FONT face="Times New Roman">0</FONT>——也就是未锁状态。</P><P 0cm 0cm 0pt">注意<FONT face="Times New Roman">12795</FONT>行代码简洁地对公平性进行了考虑并最后抛弃了它——公平是饥饿的背面,正如我们前面已经介绍过的(使得一个<FONT face="Times New Roman">CPU</FONT>或进程饥饿应被认为是“不公平的”)。</P><P 0cm 0cm 0pt">自旋锁的加锁和解锁宏建立在<B normal"><FONT face="Times New Roman">spin_lock_string</FONT></B>和<B normal"><FONT face="Times New Roman">sping_unlock_string</FONT></B>函数之上,所以这一小节只对<B normal"><FONT face="Times New Roman">spin_lock_string</FONT></B>和<B normal"><FONT face="Times New Roman">sping_unlock_string</FONT></B>函数进行详述。其它宏如果有的话只是增加了<FONT face="Times New Roman">IRQ</FONT>加锁和解锁。</P><P 12pt 0cm 3.2pt"><b>spin_lock_string</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12805</FONT>:这个宏的代码对于所有自旋锁加锁的宏都是相同的。它也被用于<FONT face="Times New Roman">x86</FONT>专用的<B normal"><FONT face="Times New Roman">lock_ kernel</FONT></B>和<B normal"><FONT face="Times New Roman">unlock_kernel</FONT></B>版本之中(它们不在本书之列,不过其常规版本则是包括的——参见<FONT face="Times New Roman">10174</FONT>和<FONT face="Times New Roman">10182</FONT>行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12807</FONT>:尝试测试和设置自旋锁的最低位,这要把内存总线锁住以便对于任何其它对同一个自旋锁的访问来说这个操作都是原子的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12808</FONT>:如果成功了,控制流程就继续向下运行;否则,<B normal"><FONT face="Times New Roman">spin_lock_string</FONT></B>函数向前跳转到第<FONT face="Times New Roman">12810</FONT>行(<B normal"><FONT face="Times New Roman">btsl</FONT></B>把这一位的原值放入<FONT face="Times New Roman">CPU</FONT>的进位标志位(<FONT face="Times New Roman">Carry flag</FONT>),这正是这里使用<B normal"><FONT face="Times New Roman">jc</FONT></B>的原因)。同样的技巧我们已经看到过三次了:跳转目标放在内核的单独一段中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12811</FONT>:在封闭的循环里不停地检测循环锁的最低位。注意<B normal"><FONT face="Times New Roman">btsl</FONT></B>和<B normal"><FONT face="Times New Roman">testb</FONT></B>以不同方式解释它们第一个操作数——对于<B normal"><FONT face="Times New Roman">btsl</FONT></B>,它是一个位状态(<FONT face="Times New Roman">bit position</FONT>),而对于<B normal"><FONT face="Times New Roman">testb</FONT></B>,它是一个位屏蔽(<FONT face="Times New Roman">bitmask</FONT>)。因此,<FONT face="Times New Roman">12811</FONT>行在测试<B normal"><FONT face="Times New Roman">spin_lock_string</FONT></B>曾在<FONT face="Times New Roman">12807</FONT>行已经试图设置(但失败了)的同一位,尽管一个使用<B normal"><FONT face="Times New Roman">$0</FONT></B>而另一个使用<B normal"><FONT face="Times New Roman">$1</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12813</FONT>:该位被清除了,所以<B normal"><FONT face="Times New Roman">spin_lock_string</FONT></B>应该再次夺取它。函数调转回第<FONT face="Times New Roman">12806</FONT>行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这个代码可以只用加上<B normal"><FONT face="Times New Roman">lock</FONT></B>前缀的两条代码加以简化:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> 1: lock ; btsl $0, %0</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> jc 1b</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>不过,使用这个简化版本的话,系统性能将明显受到损害,这因为每次循环重复内存总线都要被加锁。内核使用的版本虽然长一些,但是它可以使其它<FONT face="Times New Roman">CPU</FONT>运行的更有效,这是由于该版本只有在它有充分理由相信能够获得锁的时候才会锁住内存总线。</P><P 12pt 0cm 3.2pt"><b>spin_unlock_string</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12816</FONT>:并不很重要:只是重新设置了自旋锁的锁定位(<FONT face="Times New Roman">lock bit</FONT>)。</P><P 12pt 0cm 3.2pt"><b><FONT size=3>读/写自旋锁</FONT></b></P><P 0cm 0cm 0pt">自旋锁的一个特殊情况就是读<FONT face="Times New Roman">/</FONT>写自旋锁。这里的思想是这样的:在某些情况中,我们想要允许某个对象有多个读者,但是当有一个写者正在写入这个对象时,则不允许它再有其它读者或者写者。</P><P 0cm 0cm 0pt">遵循基于<B normal"><FONT face="Times New Roman">spinlock_t</FONT></B>的自旋锁的同样模式,读<FONT face="Times New Roman">/</FONT>写自旋锁是用<B normal"><FONT face="Times New Roman">rwlock_t</FONT></B>(<FONT face="Times New Roman">12853</FONT>行)来代表的,它可以在有<B normal"><FONT face="Times New Roman">RW_LOCK_UNLOCKED</FONT></B>(<FONT face="Times New Roman">12858</FONT>行)的声明里被初始化。与<B normal"><FONT face="Times New Roman">rwlock_t</FONT></B>一起工作的最低级的宏是<B normal"><FONT face="Times New Roman">read_lock</FONT></B>、<B normal"><FONT face="Times New Roman">read_unlock</FONT></B>、<B normal"><FONT face="Times New Roman">write_lock</FONT></B>,以及<B normal"><FONT face="Times New Roman">write_unlock</FONT></B>,它们在本小节中进行描述。很明显,那些跟随在这些宏之后并建立在它们之上的宏,自然要在你理解了最初的这四个宏之后在去接触。</P><P 0cm 0cm 0pt">正如第<FONT face="Times New Roman">12860</FONT>行注释中所声明的,当写锁(<FONT face="Times New Roman">write lock</FONT>)被占有时,<B normal"><FONT face="Times New Roman">rwlock_t</FONT></B>的<B normal"><FONT face="Times New Roman">lock</FONT></B>成员是负值。当既没有读者也没有写者时它为<FONT face="Times New Roman">0</FONT>,当只有读者而没有写者时它是正值——在这种情况下,<B normal"><FONT face="Times New Roman">lock</FONT></B>将对读者的数目进行计数。</P><P 12pt 0cm 3.2pt"><b>read_lock</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12867</FONT>:开始于<B normal"><FONT face="Times New Roman">rwlock_t</FONT></B>的<B normal"><FONT face="Times New Roman">lock</FONT></B>成员的自动递增。这是推测性的操作——它可以被撤销。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12868</FONT>:如果它在增量之后为负,表示某个进程占用了写锁——或者至少是某个进程正试图得到它。<B normal"><FONT face="Times New Roman">read_lock</FONT></B>向前跳到第<FONT face="Times New Roman">12870</FONT>行(注意,在一个不同的内核段里)。否则,没有写者退出(尽管还有可能有,或者也有可能没有其它读者——这并不重要),所以可以继续执行读锁定(<FONT face="Times New Roman">read-locked</FONT>)代码。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12870</FONT>:一个写者出现了。<B normal"><FONT face="Times New Roman">read_lock</FONT></B>取消第<FONT face="Times New Roman">12867</FONT>行增值操作的影响。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12871</FONT>:循环等待<B normal"><FONT face="Times New Roman">rwlock_t</FONT></B>的<B normal"><FONT face="Times New Roman">lock</FONT></B>变为<FONT face="Times New Roman">0</FONT>或正值。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">12873</FONT>:跳回到第<FONT face="Times New Roman">12866</FONT>行再次尝试。</P><P 12pt 0cm 3.2pt"><b>read_unlock</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12878</FONT>:不太复杂:只是递减该计数值。</P><P 12pt 0cm 3.2pt"><b>write_lock</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12883</FONT>:表示出有一个进程需要写锁:检测并设置<B normal"><FONT face="Times New Roman">lock</FONT></B>的符号位并保证<B normal"><FONT face="Times New Roman">lock</FONT></B>的值是负的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12884</FONT>:如果符号位已经被设置,则另外有进程占有了写锁;<B normal"><FONT face="Times New Roman">write_lock</FONT></B>向前跳转到第<FONT face="Times New Roman">12889</FONT>行(同以前一样,那是在一个不同的内核段里)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12885</FONT>:没有别的进程正试图获得该写锁,可是读者仍可以退出。因为符号位被设置了,读者不能获得读锁,但是<B normal"><FONT face="Times New Roman">write_lock</FONT></B>仍然必须等待正在退出的读者完全离开。它通过检查低端的<FONT face="Times New Roman">31</FONT>位中是否任何一位被设置过开始,这可以表示<B normal"><FONT face="Times New Roman">lock</FONT></B>以前曾是正值。如果没有,则<B normal"><FONT face="Times New Roman">lock</FONT></B>在符号位反转之前曾是<FONT face="Times New Roman">0</FONT>,这意味着没有读者;因而,这对于写者的继续工作是很安全的,所以控制流程就可以继续向下运行了。不过,如果低端<FONT face="Times New Roman">31</FONT>位中任何一位被设置过了,也就是说有读者了,这样<B normal"><FONT face="Times New Roman">write_lock</FONT></B>就会向前跳转到第<FONT face="Times New Roman">12888</FONT>行等到它们结束。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12888</FONT>:该进程是仅有的写者,但是有若干读者。<B normal"><FONT face="Times New Roman">write_lock</FONT></B>会暂时清除符号位(这个宏稍后将再次操纵它)。有趣的是,对符号位进行这样的胡乱操作并不会影响读者操纵<B normal"><FONT face="Times New Roman">lock</FONT></B>的正确性。考虑作为示例的下列顺序事件:</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l5 level1 lfo8; tab-stops: list 57.25pt"><FONT face="Times New Roman">1. </FONT>两个读者增加了<B normal"><FONT face="Times New Roman">lock</FONT></B>;<B normal"><FONT face="Times New Roman">lock</FONT></B>用十六进制表示现在是<FONT face="Times New Roman">0x00000002</FONT>。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l5 level1 lfo8; tab-stops: list 57.25pt"><FONT face="Times New Roman">2. </FONT>一个即将成为写者的进程设置了符号位;<B normal"><FONT face="Times New Roman">lock</FONT></B>现在是<FONT face="Times New Roman">0x80000002</FONT>。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l5 level1 lfo8; tab-stops: list 57.25pt"><FONT face="Times New Roman">3. </FONT>读者中的一个离开;<B normal"><FONT face="Times New Roman">lock</FONT></B>现在是<FONT face="Times New Roman">0x80000001</FONT>。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l5 level1 lfo8; tab-stops: list 57.25pt"><FONT face="Times New Roman">4. </FONT>写者看到剩余的位不全部是<FONT face="Times New Roman">0</FONT>——仍然有读者存在。这样它根本没有写锁,因此它就清除符号位;<B normal"><FONT face="Times New Roman">lock</FONT></B>现在是<FONT face="Times New Roman">0x00000001</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这样,读和写可以任何顺序交错尝试操作而不会影响结果的正确程度。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12889</FONT>:循环等待计数值降到<FONT face="Times New Roman">0</FONT>——也就是等待所有读者退出。实际上,<FONT face="Times New Roman">0</FONT>除了表示所有读者已离开之外,它还表示着没有其它进程获得了写锁。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">12891</FONT>:所有读者和写者都结束了操作;<B normal"><FONT face="Times New Roman">write_lock</FONT></B>又从头开始,并再次获得写锁。</P><P 12pt 0cm 3.2pt"><b>write_unlock</b></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">12896</FONT>:不太重要:只是重置符号位。</P><H2 13pt 0cm">APICs和CPU-To-CPU通信</H2><P 0cm 0cm 0pt"><FONT face="Times New Roman">Intel </FONT>多处理规范的核心就是高级可编程中断控制器(<FONT face="Times New Roman">Advanced Programmable Interrupt Controllers</FONT>——<FONT face="Times New Roman">APICs</FONT>)的使用。<FONT face="Times New Roman">CPU</FONT>通过彼此发送中断来完成它们之间的通信。通过给中断附加动作(<FONT face="Times New Roman">actions</FONT>),不同的<FONT face="Times New Roman">CPU</FONT>可以在某种程度上彼此进行控制。每个<FONT face="Times New Roman">CPU</FONT>有自己的<FONT face="Times New Roman">APIC</FONT>(成为那个<FONT face="Times New Roman">CPU</FONT>的本地<FONT face="Times New Roman">APIC</FONT>),并且还有一个<FONT face="Times New Roman">I/O APIC</FONT>来处理由<FONT face="Times New Roman">I/O</FONT>设备引起的中断。在普通的多处理器系统中,<FONT face="Times New Roman">I/O APIC</FONT>取代了第<FONT face="Times New Roman">6</FONT>章里提到的中断控制器芯片组的作用。</P><P 0cm 0cm 0pt">这里有几个示例性的函数来让你了解其工作方式的风格。</P><P 12pt 0cm 3.2pt"><b>smp_send_reschedule</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5019</FONT>:<FONT face="Times New Roman"> </FONT>这个函数只有一行,其作用将在本章随后进行说明,它仅仅是给其<FONT face="Times New Roman">ID</FONT>以参数形式给出了的目标<FONT face="Times New Roman">CPU</FONT>发送一个中断。函数用<FONT face="Times New Roman">CPU ID</FONT>和<B normal"><FONT face="Times New Roman">RESCHEDULE_VECTOR</FONT></B>向量调用<B normal"><FONT face="Times New Roman">send_IPI_single</FONT></B>函数(<FONT face="Times New Roman">4937</FONT>行)。<B normal"><FONT face="Times New Roman">RESCHEDULE_VECTOR</FONT></B>与其它<FONT face="Times New Roman">CPU</FONT>中断向量是一起在第<FONT face="Times New Roman">1723</FONT>行开始的一个定义块中被定义的。</P>
< 0cm 0cm 0pt">或调度策略。由于这比仅仅的完美级别要更适用,<B normal"><FONT face="Times New Roman">CAP_SYS_NICE</FONT></B>是一个误用的位——虽然很容易就可以看出设置调度策略和相关的概念是紧密相关的,而且你一般也不会要一个权能而不要另外一个权能。</P>< 0cm 0cm 0pt">每一个进程都有三个权能,它们被存储在进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构中(在<FONT face="Times New Roman">16400</FONT>行到<FONT face="Times New Roman">16401</FONT>行中):</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo2; tab-stops: list 42.25pt">l <B normal"><FONT face="Times New Roman">cap_effective</FONT></B>——有效置位集合</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo2; tab-stops: list 42.25pt">l <B normal"><FONT face="Times New Roman">cap_permitted</FONT></B>——允许位集合</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo2; tab-stops: list 42.25pt">l <B normal"><FONT face="Times New Roman">cap_inheritable</FONT></B>——继承位集合</P>< 0cm 0cm 0pt">进程权能的有效位集合是当前可以处理的内容的集合;这是通过广泛使用的<B normal"><FONT face="Times New Roman">capable</FONT></B>函数检测的集合,这个函数在<FONT face="Times New Roman">16738</FONT>行定义。</P>< 0cm 0cm 0pt">允许位集合规定进程正常地可以被赋予的权能。这个集合通常不会增加——只有一种情况例外:如果一个进程具有<B normal"><FONT face="Times New Roman">CAP_SETPCAP</FONT></B>权能,那么它就可以将自己的允许位集合中的任何权能赋给其它进程,即使目标进程还没有拥有这个权能。</P>< 0cm 0cm 0pt">如果一个权能在允许位集合中,但是并不在有效位集合中,那么进程现在还没有马上拥有权能,但是它可以通过请求权能而获得。为什么要麻烦地区别它们呢?在本章开始我们第一次讨论权能的时候,我们简单地考虑了一个简单的例子:一个长期运行的进程只是偶然需要权能,而不是所有情况下都需要。为了保证进程不会偶然缺少权能,进程可以一直等待,直到它需要权能,接着请求权能,执行有权限的操作,并再次取消权能。这种方法比较安全。</P>< 0cm 0cm 0pt">继承位集合不像你想象的那么简单。它不是祖先继承在执行<B normal"><FONT face="Times New Roman">fork</FONT></B>的同时传递的权能集合——实际上,在创建的那一刻(也就是紧随着<B normal"><FONT face="Times New Roman">fork</FONT></B>),子孙进程的权能的三个集合和其祖先的三个权能集合都是相同的。相反,继承位集合在<B normal"><FONT face="Times New Roman">exec</FONT></B>运行期间才会起作用。进程在调用<B normal"><FONT face="Times New Roman">exec</FONT></B>之前的继承位集合有助于决定它的允许位集合和继承位集合,它们在<B normal"><FONT face="Times New Roman">exec</FONT></B>执行结束以后也会保留下来——仔细的介绍请参看<B normal"><FONT face="Times New Roman">compute_creds</FONT></B>(<FONT face="Times New Roman">9948</FONT>行)。注意在<B normal"><FONT face="Times New Roman">exec</FONT></B>之后权能是否保留要部分依赖于进程的继承位集合;它还要部分依赖于文件本身中的权能位集合(或者不管怎样,这至少是一个计划——虽然这种特性还没有完全实现)。</P>< 0cm 0cm 0pt">顺便提一下,注意到允许位集合必须总是有效位集合和继承位集合的超集(<FONT face="Times New Roman">superset</FONT>)(或者和有效位集合相同)。(只有对于有效位集合这才是严格正确的。一个进程可能会扩展另外一个进程的继承位集合从而它不再是其允许位集合的子集,但是就我知道的来说,这是无意义的,因此我们从现在就开始忽略这种可能性。)然而,和你可能希望的相反,有效位集合不一定要是继承位集合的超集(或者和继承位集合相同)。也就是说,在<B normal"><FONT face="Times New Roman">exec</FONT></B>结束以后,进程可能会拥有一个以前不曾有过的权能(虽然这个权能必须在其允许位集合中——也就是说,这是一个原来进程自己可能已经得到了的权能)。我认为这种需要只是局部的,这样进程就不需要暂时获得不需要的权能,而能够获得足以执行<B normal"><FONT face="Times New Roman">exec</FONT></B>程序的权能。</P>< 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt"><wrapblock><v:shapetype><v:stroke joinstyle="miter"></v:stroke><v:formulas><v:f eqn="if lineDrawn pixelLineWidth 0"></v:f><v:f eqn="sum @0 1 0"></v:f><v:f eqn="sum 0 0 @1"></v:f><v:f eqn="prod @2 1 2"></v:f><v:f eqn="prod @3 21600 pixelWidth"></v:f><v:f eqn="prod @3 21600 pixelHeight"></v:f><v:f eqn="sum @0 0 1"></v:f><v:f eqn="prod @6 1 2"></v:f><v:f eqn="prod @7 21600 pixelWidth"></v:f><v:f eqn="sum @8 21600 0"></v:f><v:f eqn="prod @7 21600 pixelHeight"></v:f><v:f eqn="sum @10 21600 0"></v:f></v:formulas><v:path connecttype="rect" gradientshapeok="t" extrusionok="f"></v:path><lock aspectratio="t" v:ext="edit"></lock></v:shapetype><v:shape><FONT face="Times New Roman"><v:imagedata></v:imagedata><w:wrap type="topAndBottom"></w:wrap></FONT></v:shape></wrapblock><BR vglayout" clear=all></P>< 0cm 0cm 0pt; TEXT-ALIGN: center; mso-outline-level: 1" align=center>图<FONT face="Times New Roman">7.4 </FONT>权能集</P>< 0cm 0cm 0pt; TEXT-ALIGN: center; mso-outline-level: 1" align=center><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt">图<FONT face="Times New Roman">7.4</FONT>说明了各种可能性。它显示了一个理想进程的三种权能集合,位从左到右计数。允许进程可以获得<B normal"><FONT face="Times New Roman">CAP_KILL</FONT></B>权能,这样就允许它不考虑其它属主而杀掉别的进程,但是它还没有立即拥有权限,而且也不会在<B normal"><FONT face="Times New Roman">exec</FONT></B>执行过程中自动获得。目前它具有增加和删除内核模块的权能(使用<B normal"><FONT face="Times New Roman">CAP_SYS_MODULE</FONT></B>),但是同样也不会在<B normal"><FONT face="Times New Roman">exec</FONT></B>执行过程中自动获得。它可以获得<B normal"><FONT face="Times New Roman">CAP_SYS_NICE</FONT></B>权能,但是直到<B normal"><FONT face="Times New Roman">exec</FONT></B>执行完后才会获得(假定文件权能位允许)。最后,它可以立即修改系统时间(<B normal"><FONT face="Times New Roman">CAP_SYS_TIME</FONT></B>),但是也是只有通过<B normal"><FONT face="Times New Roman">exec</FONT></B>才能获得这个权能。除非其它具有<B normal"><FONT face="Times New Roman">CAP_SETPCAP</FONT></B>权能的进程提供了这个权能,否则这个进程不能获得这个权能,它可能执行的其它进程也不可能获得这个权能。</P>< 0cm 0cm 0pt">保证这些不同性质的代码主要是在<FONT face="Times New Roman">kernel/capability.c</FONT>中,从<FONT face="Times New Roman">22460</FONT>行开始。两个主要的函数是读取权能的函数<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>(<FONT face="Times New Roman">22480</FONT>行)和设置权能的函数<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>(<FONT face="Times New Roman">22592</FONT>行);它们在下一节中讨论。通过<B normal"><FONT face="Times New Roman">exec</FONT></B>继承的权能使用<FONT face="Times New Roman">fs/exec.c</FONT>的<B normal"><FONT face="Times New Roman">compute_creds</FONT></B>(<FONT face="Times New Roman">9948</FONT>行)处理,这一点已经介绍过了。</P>< 0cm 0cm 0pt">当然,<FONT face="Times New Roman">root</FONT>肯定拥有所有的权能。内核权能特性给<FONT face="Times New Roman">root</FONT>提供了一种规则的方法来有所选择地只把需要的权能赋给特定的进程,而不用考虑该进程是否作为<FONT face="Times New Roman">root</FONT>用户运行。</P>< 0cm 0cm 0pt">权能一个有趣的特性是它们可以用来改变系统的“风格”。作为一个简单的例子,为所有的进程设置<B normal"><FONT face="Times New Roman">CAP_SYS_NICE</FONT></B>权能会使所有进程都增加自己的优先级(并设置它们的调度规则,等等)。如果你修改了系统中每一个进程的运行方式,那么你就改变了系统本身。自己设想一下发明一种新的可以通过更令人兴奋的方式修补系统的内核权能。</P>< 0cm 0cm 0pt">权能的尚未为人所知的优点是它们使源程序代码非常清晰。当检测当前进程是否允许设置系统时间时,却反而要检测当前进程是否以<FONT face="Times New Roman">root</FONT>运行,这种方式看起来似乎有些不很好。权能使我们可以了解它们的意思。权能的存在甚至还能够使查询进程的用户<FONT face="Times New Roman">ID</FONT>或组<FONT face="Times New Roman">ID</FONT>的代码更为清晰,这是因为这样的处理代码对这个问题的答案比较感兴趣,而是对从其中可以推导出的结论更感兴趣。否则,代码应该已经使用权能查询它需要了解的内容了。由于权能更加一致地和<FONT face="Times New Roman">Linux</FONT>内核代码结合起来,这种特性就变得更加可靠了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">13916</FONT>:内核可以识别的权能从这里开始。因为这些宏定义的解释已非常详细了,我们就不再详细介绍其中每一个的内容了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">14153</FONT>:赋给每一个权能的数字是简单的连续整数,但是由于要使用无符号整数中的位来编址,所以就使用<B normal"><FONT face="Times New Roman">CAP_TO_MASK</FONT></B>宏把它们转化为<FONT face="Times New Roman">2</FONT>的幂。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">14154</FONT>:设置和检测权能的核心只是一系列位操作;从这里到<FONT face="Times New Roman">include/linux/capability.h</FONT>中定义了用来使位操作更为清晰的宏和内联函数。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_capget</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22480</FONT>:<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>有两个参数:<B normal"><FONT face="Times New Roman">header</FONT></B>和<B normal"><FONT face="Times New Roman">dataptr</FONT></B>。<B normal"><FONT face="Times New Roman">header</FONT></B>是<B normal"><FONT face="Times New Roman">cap_user_header_t</FONT></B>类型(<FONT face="Times New Roman">13878</FONT>行)的,它是一个指向定义权能使用的版本和目标进程的<FONT face="Times New Roman">ID</FONT>的结构的指针;<B normal"><FONT face="Times New Roman">dataptr</FONT></B>是<B normal"><FONT face="Times New Roman">cap_user_data_t</FONT></B>类型(<FONT face="Times New Roman">13884</FONT>行)的,它也是一个指向结构类型的指针——这个结构包含有效位、允许位和继承位集合。<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>通过第二个指针返回信息。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22492</FONT>:在版本不匹配的情况下,<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>通过<B normal"><FONT face="Times New Roman">header</FONT></B>指针返回使用的版本,接着返回<FONT face="Times New Roman"><B normal">EINVA</B>L</FONT>错误(或者如果它不能把版本信息拷贝到调用者的空间中就返回<B normal"><FONT face="Times New Roman">EFAULT</FONT></B>)。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22509</FONT>:定义调用者希望了解其权能的进程;如果<B normal"><FONT face="Times New Roman">pid</FONT></B>不是<FONT face="Times New Roman">0</FONT>,也不是当前进程的<FONT face="Times New Roman">ID</FONT>,<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>就要查询它。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22520</FONT>:如果它能装载目标进程,它就把自己的权能拷贝到临时变量<B normal"><FONT face="Times New Roman">data</FONT></B>中。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22530</FONT>:如果所有工作到目前为止都运行良好,它就把权能拷贝回用户空间中由<B normal"><FONT face="Times New Roman">dataptr</FONT></B>参数提供的地址中。然后,它返回<B normal"><FONT face="Times New Roman">error</FONT></B>变量——通常如果一切运行良好,这就是<FONT face="Times New Roman">0</FONT>;否则就是一个错误号。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_capset</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22592</FONT>:<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>的参数几乎和<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>的参数类似。不同之处是<B normal"><FONT face="Times New Roman">data</FONT></B>(不再称为<B normal"><FONT face="Times New Roman">dataptr</FONT></B>了)是常量。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22600</FONT>:和<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>一样,<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>确保内核和调用进程使用一致的权能系统的版本。如果版本不一致,就拒绝尝试请求。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22613</FONT>:如果<B normal"><FONT face="Times New Roman">pid</FONT></B>不是<FONT face="Times New Roman">0</FONT>,就说明调用者希望设置其它进程的权能,在大多数情况下这种尝试都会遭到拒绝。如果调用者具有<B normal"><FONT face="Times New Roman">CAP_SETPCAP</FONT></B>权能,这意味着允许它设置任何进程的权能,<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>就允许这种尝试。这种测试的前面部分有些太受限制了:如果它和当前进程的<B normal"><FONT face="Times New Roman">pid</FONT></B>相等,就接收这个<B normal"><FONT face="Times New Roman">pid</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22616</FONT>:从用户空间中拷贝新的权能,如果失败就返回错误。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22627</FONT>:和<FONT face="Times New Roman">22509</FONT>行开始的<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>代码类似,<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>定义了调用者希望了解其权能的进程。这就是两者的区别所在,<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>为了说明进程组(或者是<FONT face="Times New Roman">-1</FONT>指明是所有进程)也允许其<B normal"><FONT face="Times New Roman">pid</FONT></B>值为负。在这种情况下,<B normal"><FONT face="Times New Roman">target</FONT></B>仍然设置为<B normal"><FONT face="Times New Roman">current</FONT></B>,因此当前进程的权能要在后面的计算中使用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22642</FONT>:现在它必须保证合法地使用新的权能位集合,而且在内部保持一致。除非这种新特性在调用者的允许位集合中,否则这种测试会验证出新进程的继承位集合没有包含任何新鲜的东西。因此,它不会放弃调用者尚未拥有的任何权能。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22650</FONT>:类似地,<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>也要确保除非调用者的允许位中包含新的特性,否则目标进程的允许位集合也不会包含尚未具有的特性。因此,它也不会放弃调用者尚未拥有的任何权能。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22658</FONT>:回想一下进程的有效位集合必须是其允许位集合的一个子集。这种性质在这里得到了保证。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22666</FONT>:<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>现在已经准备对请求做出修改。负的<B normal"><FONT face="Times New Roman">pid</FONT></B>值意味着它正在给不止一个进程修改权能——如果<B normal"><FONT face="Times New Roman">pid</FONT></B>是<FONT face="Times New Roman">-1</FONT>,就是所有的进程;如果<B normal"><FONT face="Times New Roman">pid</FONT></B>是其它的负值,就是一个进程组中的所有进程。在这些情况下,实际工作分别由<B normal"><FONT face="Times New Roman">cap_set_all</FONT></B>(<FONT face="Times New Roman">22561</FONT>行)和<B normal"><FONT face="Times New Roman">cap_set_pg</FONT></B>(<FONT face="Times New Roman">22539</FONT>行)完成;这只是通过一些适当的进程集合循环,按照和单个进程相同的方法覆盖掉集合中的每一个进程的权能位集合。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22676</FONT>:如果<B normal"><FONT face="Times New Roman">pid</FONT></B>是正数(或者是<FONT face="Times New Roman">0</FONT>,表示当前进程),权能位集合只赋给目标进程。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>用户<FONT face="Times New Roman">ID</FONT>和组<FONT face="Times New Roman">ID</FONT></FONT></H3><P 0cm 0cm 0pt">尽管权能功能强大、十分有用,但它并不是你实现访问控制的唯一武器。在一些情况中,我们需要了解哪个用户正在运行一个进程,或者进程是作为哪个用户来运行。用户使用整型的用户<FONT face="Times New Roman">ID</FONT>来区别,一个用户可以属于一个组或者多个组,每一个都有自己特有的整型<FONT face="Times New Roman">ID</FONT>。</P><P 0cm 0cm 0pt">有两种风格的用户<FONT face="Times New Roman">ID</FONT>和组<FONT face="Times New Roman">ID</FONT>:实际的<FONT face="Times New Roman">ID</FONT>和有效的<FONT face="Times New Roman">ID</FONT>。一般说来,实际用户(或组)<FONT face="Times New Roman">ID</FONT>为你说明了哪个用户创建了进程,有效用户(或组)<FONT face="Times New Roman">ID</FONT>为你说明在情况改变时进程作为哪个用户运行。由于访问控制的决定要更多依赖于进程作为哪儿用户运行,而不是哪个用户创建了这个进程,因此内核会比检测实际用户(和组)<FONT face="Times New Roman">ID</FONT>更加频繁地检测有效用户(或)<FONT face="Times New Roman">ID</FONT>——在我们现在关心的代码中就是这样处理的。<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构中的相关成员是<B normal"><FONT face="Times New Roman">uid</FONT></B>,<B normal"><FONT face="Times New Roman">euid</FONT></B>,<B normal"><FONT face="Times New Roman">gid</FONT></B>,和<B normal"><FONT face="Times New Roman">egid</FONT></B>(<FONT face="Times New Roman">16396</FONT>行到<FONT face="Times New Roman">16397</FONT>行)。注意用户<FONT face="Times New Roman">ID</FONT>和用户名不同,前者是一个整数,而后者是一个字符串。<FONT face="Times New Roman">/etc/passwd</FONT>文件把这两者关联起来。</P><P 0cm 0cm 0pt">让我们再回到<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>并看一下前面我们忽略了的从<FONT face="Times New Roman">29244</FONT>行到<FONT face="Times New Roman">29245</FONT>行的一些代码。<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>通常执行的操作都是让用户降低自己进程的优先级,但是不能降低其它用户进程的优先级——除非用户具有<B normal"><FONT face="Times New Roman">CAP_SYS_NICE</FONT></B>权能。因此,<FONT face="Times New Roman">if</FONT>表达式的前面两个术语要检测目标进程的用户<FONT face="Times New Roman">ID</FONT>是否和<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>的调用者的实际用户<FONT face="Times New Roman">ID</FONT>或者有效用户<FONT face="Times New Roman">ID</FONT>匹配。如果两个都不匹配,并且<B normal"><FONT face="Times New Roman">SYS_CAP_NICE</FONT></B>没有设置,<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>就正确地拒绝这种尝试。</P><P 0cm 0cm 0pt">如果允许,进程可以使用<B normal"><FONT face="Times New Roman">sys_setuid</FONT></B>和<B normal"><FONT face="Times New Roman">sys_setgid</FONT></B>(<FONT face="Times New Roman">29578</FONT>行和<FONT face="Times New Roman">29445</FONT>行)和其它一些函数修改它们的用户<FONT face="Times New Roman">ID</FONT>和组<FONT face="Times New Roman">ID</FONT>。用户<FONT face="Times New Roman">ID</FONT>和组<FONT face="Times New Roman">ID</FONT>也可以通过执行可执行的<FONT face="Times New Roman">setuid</FONT>或<FONT face="Times New Roman">setgid</FONT>可执行程序进行修改。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>资源限制</FONT></H3><P 0cm 0cm 0pt">可以要求内核限制一个进程使用系统中的各种资源,包括内存和<FONT face="Times New Roman">CPU</FONT>时间。这可以通过<B normal"><FONT face="Times New Roman">sys_setrlimit</FONT></B>实现(<FONT face="Times New Roman">30057</FONT>行)。通过浏览<B normal"><FONT face="Times New Roman">struct rusage</FONT></B>结构(<FONT face="Times New Roman">16068</FONT>行)你对支持限制就可以有一个基本的概念。进程特有的限制在<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构中记录——还可能在什么地方?请参看<FONT face="Times New Roman">16404</FONT>行的<B normal"><FONT face="Times New Roman">rlim</FONT></B>数组成员。</P><P 0cm 0cm 0pt">违反限制的结果根据限制的不同也会有所不同。例如,对于<B normal"><FONT face="Times New Roman">RLIMIT_MPROC</FONT></B>(在本书的源程序代码中没有包括)——有关一个用户可以拥有的进程数目的限制——和你在<FONT face="Times New Roman">23974</FONT>行中看到的一样,结果仅仅和<B normal"><FONT face="Times New Roman">fork</FONT></B>失败一样。超出其它限制的后果对于一些进程可能比较严重,这样进程会被杀死(请参看<FONT face="Times New Roman">27333</FONT>行)。进程可以使用<B normal"><FONT face="Times New Roman">sys_getrlimit</FONT></B>(<FONT face="Times New Roman">30046</FONT>行)请求特殊限制,或者使用<B normal"><FONT face="Times New Roman">sys_getrusage</FONT></B>(<FONT face="Times New Roman">30143</FONT>行)请求资源使用限制。</P><P 0cm 0cm 0pt">在<FONT face="Times New Roman">30067</FONT>行中,注意进程可以随意减少自己的资源限制,但是它增加自己的资源限制时只能增加到一个最大值,这个值可以根据每一个资源限制进行具体设置。因此,当前的资源限制和所有的资源限制是分别记录的(使用在<FONT face="Times New Roman">16089</FONT>行定义的<B normal"><FONT face="Times New Roman">struct rlimit</FONT></B>结构的<B normal"><FONT face="Times New Roman">rlin_cur</FONT></B>成员和<B normal"><FONT face="Times New Roman">rlim_max</FONT></B>成员)。然而具有<B normal"><FONT face="Times New Roman">CAP_SYS_RESOURCE</FONT></B>权能的进程可以覆盖这个最大值。</P><P 0cm 0cm 0pt">这和优先级的规则不同:允许进程可以减小自己的优先级,但是为增加其优先级需要特殊许可,即使是它减少了自己的优先级接着又要马上增加它也是如此。当前资源限制和最大资源限制这两个相互关联的概念并没有反映在内核优先级的调度中。还有,注意到一个进程可以改变另一个进程的优先级(当然是假定它有权这样处理),但是一个进程只能修改自己的资源限制。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">所有美好的事物都会结束——这就是它们如何处理的</H2><P 0cm 0cm 0pt">我们已经看到进程是如何生成的,怎样给它们赋予各自的生存周期。现在我们应该看一下它们是如何消亡的。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT face="Times New Roman" size=5>exit</FONT></H3><P 0cm 0cm 0pt">同第<FONT face="Times New Roman">6</FONT>章中介绍的一样,你可以通过给进程发送信号量<FONT face="Times New Roman">9</FONT>强行杀掉进程,但是更普通的情况是进程自动退出。进程通过调用系统调用<B normal"><FONT face="Times New Roman">exit</FONT></B>自动退出,它在内核中是由<B normal"><FONT face="Times New Roman">sys_exit</FONT></B>实现的(<FONT face="Times New Roman">23322</FONT>行)。(顺便说一下,当<FONT face="Times New Roman">C</FONT>程序从它的<B normal"><FONT face="Times New Roman">main</FONT></B>部分返回时,就会潜在调用<B normal"><FONT face="Times New Roman">exit</FONT></B>。)当进程退出时,内核释放所有分配给这个进程的资源——内存、文件,等等——当然,还要停止给它继续使用<FONT face="Times New Roman">CPU</FONT>的机会。</P><P 0cm 0cm 0pt">然而内核不能立即回收代表进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构,这是因为该进程的祖先必须能够使用<B normal"><FONT face="Times New Roman">wait</FONT></B>系统调用查询其子孙进程的退出状态。<B normal"><FONT face="Times New Roman">wait</FONT></B>返回它检测出的死亡状态的进程的<FONT face="Times New Roman">PID</FONT>,因此如果死亡的子孙进程在祖先进程仍在等待时就已经重新分配了,那么应用程序就会被搞乱(和其它问题一样,同一个祖先结束时可以有两个具有相同<FONT face="Times New Roman">PID</FONT>的子孙进程——一个进程是活动的,另一个进程是死亡的——祖先进程也不知道哪一个已经退出了)。因此,内核必须保留死亡子孙进程的<FONT face="Times New Roman">PID</FONT>直到<B normal"><FONT face="Times New Roman">wait</FONT></B>发生为止——这通过完整地保持其<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构来自动实现的;分配<FONT face="Times New Roman">PID</FONT>的代码就不用再查询它在任务列表中发现的进程是否是活动的。</P><P 0cm 0cm 0pt">处于这种在两种状态之间的进程——它既不是活动的,也没有真正死亡——被称为僵进程(<FONT face="Times New Roman">zombies</FONT>)。那么<B normal"><FONT face="Times New Roman">sys_exit</FONT></B>的任务就是把活动进程转化为僵进程。</P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">sys_exit</FONT></B>本身的工作很少;它只是简单地把现存退出代码转化为<B normal"><FONT face="Times New Roman">do_exit</FONT></B>希望的格式,接着就会调用<B normal"><FONT face="Times New Roman">do_exit</FONT></B>,由它来处理实际的工作。(<B normal"><FONT face="Times New Roman">do_exit</FONT></B>也会作为发送信号量的一部分来调用,这一点我们在第<FONT face="Times New Roman">6</FONT>章中已经讨论过了。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23267</FONT>:<B normal"><FONT face="Times New Roman">do_exit</FONT></B>把退出代码作为参数处理,在其返回类型之前使用特殊符号<B normal"><FONT face="Times New Roman">NORET_TYPE</FONT></B>。虽然现在<B normal"><FONT face="Times New Roman">NORET_TYPE</FONT></B>(<FONT face="Times New Roman">14955</FONT>行)定义为空——因此它也就不起作用——但是原来它经常被定义为<B normal"><FONT face="Times New Roman">__volatile__</FONT></B>,用来提示<FONT face="Times New Roman">gcc</FONT>该函数不会返回。了解了这一点知识,<FONT face="Times New Roman">gcc</FONT>就执行一些额外的优化工作并取消有关函数不能成功返回的警告信息。使用其新的定义,<B normal"><FONT face="Times New Roman">NORET_TYPE</FONT></B>对于编译器就没有用处了,但是它仍然给我们人类传递了很多有用的信息。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23285</FONT>:释放它的信号量和其它<FONT face="Times New Roman">System V IPC</FONT>结构,这一点我们将在第<FONT face="Times New Roman">9</FONT>章中介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23286</FONT>:释放分配给它的内存,这一点我们在第<FONT face="Times New Roman">8</FONT>章中介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23290</FONT>:释放分配给它的文件,很快就会讨论。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23291</FONT>:释放它的文件系统数据,它超出了本书的范围。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23292</FONT>:释放它的信号量处理程序表,这一点我们在第<FONT face="Times New Roman">6</FONT>章中介绍过了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23294</FONT>:剩下的任务是进入<B normal"><FONT face="Times New Roman">TASK_ZOMBIE</FONT></B>状态,其退出代码被记录下来以供将来祖先进程使用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23296</FONT>:调用<B normal"><FONT face="Times New Roman">exit_notify</FONT></B>(<FONT face="Times New Roman">23198</FONT>行),它会警告当前退出任务的祖先进程和其进程组中的所有成员该进程正在退出。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23304</FONT>:调用<B normal"><FONT face="Times New Roman">schedule</FONT></B>(<FONT face="Times New Roman">26686</FONT>行)释放<FONT face="Times New Roman">CPU</FONT>。这个对于<B normal"><FONT face="Times New Roman">schedule</FONT></B>的调用从来不会返回,这是因为它跳转到下一个进程的上下文,从来不会再跳转回来,因此这是现在退出的进程的最后一次拥有<FONT face="Times New Roman">CPU</FONT>的机会。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">__exit_files</H4><P 0cm 0cm 0pt">进程如何和文件交互不是本书的主题。但是我们应该快速浏览一下<B normal"><FONT face="Times New Roman">__exit_files</FONT></B>(<FONT face="Times New Roman">23109</FONT>行),因为这样会有助于我们理解<B normal"><FONT face="Times New Roman">__clone</FONT></B>函数,这个函数使祖先进程和子孙进程可以共享特定的信息。祖先进程和子孙进程可以共享的一种信息是它们打开的文件列表。和当时说明的一样,<FONT face="Times New Roman">Linux</FONT>使用引用计数器规则来保证进程退出之后可以正确地处理扫尾工作。这里就有个扫尾工作的很好的例子。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23115</FONT>:假设进程已经打开了文件(几乎总会是这样的),<B normal"><FONT face="Times New Roman">__exit_files</FONT></B>会递减原来存储在<B normal"><FONT face="Times New Roman">tsk->files->count</FONT></B>中的引用计数器。诸如<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>之类的原子操作将在第<FONT face="Times New Roman">10</FONT>章详细介绍;知道<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>(<FONT face="Times New Roman">10249</FONT>行)递减其参数值并当参数新值是<FONT face="Times New Roman">0</FONT>时返回真值就足够了。因此,如果<B normal"><FONT face="Times New Roman">tsk</FONT></B>的对于目标<B normal"><FONT face="Times New Roman">struct files_struct</FONT></B>结构的引用是最后一次时,这就是正确的。(如果这是一个私有拷贝,没有和其它任何进程共享,那么引用计数器的初始值就是<FONT face="Times New Roman">1</FONT>,当然它被减小为<FONT face="Times New Roman">0</FONT>。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23116</FONT>:在释放记录进程的打开文件的内存之前,必须把这些文件都关闭,这是通过调用<B normal"><FONT face="Times New Roman">close_files</FONT></B>(<FONT face="Times New Roman">23081</FONT>行)实现的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23118</FONT>:释放保留进程的文件描述符数组<B normal"><FONT face="Times New Roman">fd</FONT></B>的内存,这个数组是<B normal"><FONT face="Times New Roman">files</FONT></B>的一个子域。打开文件(<B normal"><FONT face="Times New Roman">NR_OPEN</FONT></B>,在<FONT face="Times New Roman">15067</FONT>行中定义<FONT face="Times New Roman">1,024</FONT>)的最大数量要加以选择,这样本行中的<B normal"><FONT face="Times New Roman">if</FONT></B>测试就能正确——<B normal"><FONT face="Times New Roman">fd</FONT></B>数组必须刚好适合一个内存页的大小。这样做可以使得内存的分配(或释放)速度快许多;否则,<B normal"><FONT face="Times New Roman">__exit_files</FONT></B>只好使用更通用但是速度却慢得多的内核的内存函数了。下一章会加深你对这种决策的理解。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23122</FONT>:最后,<B normal"><FONT face="Times New Roman">__exit_files</FONT></B>释放<B normal"><FONT face="Times New Roman">files</FONT></B>本身。</P><P 0cm 0cm 0pt">其它<B normal"><FONT face="Times New Roman">__exit_xxx</FONT></B>函数背后的概念是类似的:它们减少了任务自有的对于潜在共享信息的引用计数器,如果这是最后一次引用,它们要负责执行所有必须的工作来将其清除。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT face="Times New Roman" size=5>wait</FONT></H3><P 0cm 0cm 0pt">和<B normal"><FONT face="Times New Roman">exec</FONT></B>一样,<B normal"><FONT face="Times New Roman">wait</FONT></B>是一组函数,而不是一个函数。(但是和<B normal"><FONT face="Times New Roman">exec</FONT></B>不同,<B normal"><FONT face="Times New Roman">wait</FONT></B>家族的函数实际包含一个名为<B normal"><FONT face="Times New Roman">wait</FONT></B>的函数。)<B normal"><FONT face="Times New Roman">wait</FONT></B>家族中的其它函数最终都是使用一个系统调用<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>(<FONT face="Times New Roman">23327</FONT>行)实现的,这个系统调用的名字反映出它实现了<B normal"><FONT face="Times New Roman">wait</FONT></B>家族中最通用的函数<B normal"><FONT face="Times New Roman">wait4</FONT></B>。标准<FONT face="Times New Roman">C</FONT>库<FONT face="Times New Roman">libc</FONT>的实现必须重新组织对于其它<B normal"><FONT face="Times New Roman">wait</FONT></B>函数调用的参数并调用<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>。(这还不是问题的全部:由于历史的原因,内核到<FONT face="Times New Roman">Alpha</FONT>的移植也会提供<B normal"><FONT face="Times New Roman">sys_waitpid</FONT></B>。但是即使是<B normal"><FONT face="Times New Roman">sys_waitpid</FONT></B>也会反过来调用<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>。)</P><P 0cm 0cm 0pt">除了处理一些其它内容,<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>——也只有<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>——最终把僵进程送进坟墓。然而从应用程序的观点来看,<B normal"><FONT face="Times New Roman">wait</FONT></B>和相关函数要检测子孙进程的状态:检测是否有进程死亡了,如果有,到底是哪一个进程,这个进程是怎样死亡的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_wait4</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23327</FONT>:为了适合作为相当通用的一个函数,<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>有很多参数,其中一些是可选的。和通常情况一样,<B normal"><FONT face="Times New Roman">pid</FONT></B>是目标进程的<FONT face="Times New Roman">PID</FONT>;和你看到的一样,<FONT face="Times New Roman">0</FONT>和负值是特殊的。如果<B normal"><FONT face="Times New Roman">stat_addr</FONT></B>非空,那么它就是所得子孙进程的退出状态应该拷贝到的地址。<B normal"><FONT face="Times New Roman">options</FONT></B>是一些可能定义<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>的操作的标志的集合。如果<B normal"><FONT face="Times New Roman">ru</FONT></B>非空,那么它就是所获得的子孙进程资源使用信息所应该拷贝到的地址。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23335</FONT>:如果提供了无效选项,<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>就返回错误代码。这种决定看起来有点荒唐;我们可以简单忽略一些无关选项。当然,这样处理所需要的参数,如果调用者设置了自己不想设置的位,那么希望的操作是不要执行——在任何情况下,这都意味着调用者不能正确理解,在这种情况下发送一个失败信号量要比简单地忽略调用者的这种困惑要更多。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23342</FONT>:循环遍历该进程的直接子进程(但不包括其孙进程,曾孙进程,等等)。如同本章中前面说明的一样,进程的最年轻(最近创建的)子孙进程通过<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">p_cptr</FONT></B>成员是可访问的,这个最年轻进程原来的兄弟进程通过其<B normal"><FONT face="Times New Roman">p_osptr</FONT></B>成员也是可以访问的;因此,<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>从这个最年轻子孙进程开始遍历其祖先的所有子孙进程,并逐渐遍历其原来的兄弟进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23343</FONT>:根据<B normal"><FONT face="Times New Roman">pid</FONT></B>参数的值筛选出不匹配的<FONT face="Times New Roman">PID</FONT>。注意值为<FONT face="Times New Roman">-1</FONT>的<B normal"><FONT face="Times New Roman">pid</FONT></B>参数是如何潜在的对进程进行选择的,正如我们所期望的:<B normal"><FONT face="Times New Roman">pid</FONT></B>值在<FONT face="Times New Roman">23343</FONT>,<FONT face="Times New Roman">23346</FONT>和<FONT face="Times New Roman">23349</FONT>行中的测试没有成功,因此它就不会遭到拒绝。这样,系统需要对每一个子孙进程进行考虑。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23376</FONT>:这就是我们现在感兴趣的情况——祖先进程正在等待一个已经结束了的进程。这是最后实际上得到僵进程的地方。它通过更新子孙进程使用的进程的用户时间和系统时间部分开始(这通过<FONT face="Times New Roman">29772</FONT>行的<B normal"><FONT face="Times New Roman">sys_times</FONT></B>系统调用实现),因为子孙进程不会再参与计算了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23382</FONT>:其它资源使用信息被收集起来(如果要求这样处理),子孙进程的退出状态被传递到特定的地址中(同样,如果要求这样处理)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23387</FONT>:设置<B normal"><FONT face="Times New Roman">retval</FONT></B>为当前得到的死亡子孙进程的<FONT face="Times New Roman">PID</FONT>。这就是最后的结果;<B normal"><FONT face="Times New Roman">retval</FONT></B>不会再改变了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23388</FONT>:如果这个垂死进程的当前祖先进程不是原来的祖先进程,那么进程就会离开进程图表中的当前位置(通过<B normal"><FONT face="Times New Roman">REMOVE_LINKS</FONT></B>,<FONT face="Times New Roman">16876</FONT>行),在其原始祖先的控制下重新安装自己(通过<B normal"><FONT face="Times New Roman">SET_LINKS</FONT></B>,<FONT face="Times New Roman">16887</FONT>行),接着给其祖先进程发送<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>信号量,这样祖先进程就知道其子孙进程已经退出了。这种通知是通过<B normal"><FONT face="Times New Roman">notify_parent</FONT></B>(<FONT face="Times New Roman">28548</FONT>行,在第<FONT face="Times New Roman">6</FONT>章中介绍)传递的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23396</FONT>:否则——正常情况——最后可以调用<B normal"><FONT face="Times New Roman">release</FONT></B>(<FONT face="Times New Roman">22951</FONT>行)释放所得子孙进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构。(在看完<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>以后,我们马上就会看<B normal"><FONT face="Times New Roman">release</FONT></B>。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23400</FONT>:现在已经成功获取了子孙进程,因此<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>只需要返回成功信息就完美地完成了工作;它跳转到<FONT face="Times New Roman">23418</FONT>行,从这儿返回<B normal"><FONT face="Times New Roman">retval</FONT></B>(所获得子孙进程的<FONT face="Times New Roman">PID</FONT>)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23401</FONT>:注意特殊的流程控制;<B normal"><FONT face="Times New Roman">default</FONT></B>的情况需要继续执行从<FONT face="Times New Roman">23342</FONT>行开始的<B normal"><FONT face="Times New Roman">for</FONT></B>循环。因为只有既没有停止运行也不是僵进程的进程才会执行到<B normal"><FONT face="Times New Roman">default</FONT></B>的情况,所以这种流程控制是正确的,但是初次阅读时比较容易误解。而且,无论如何这也有些多余;没有它循环也一样能处理。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23406</FONT>:如果代码能运行到此处,<B normal"><FONT face="Times New Roman">for</FONT></B>循环就可以完整地运行下来——正在调用的进程遍历执行其子孙进程没有发现匹配的整个列表——计算的结果是三种状态中的一种。或者由于该任务没有和所提供的<B normal"><FONT face="Times New Roman">pid</FONT></B>参数匹配的子孙进程,因而还没有进程退出,或者(是前面情况的一个特例)该任务根本就没有子孙进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23408</FONT>:如果<B normal"><FONT face="Times New Roman">flag</FONT></B>不为<FONT face="Times New Roman">0</FONT>,在<B normal"><FONT face="Times New Roman">for</FONT></B>循环中就可以执行到<FONT face="Times New Roman">23358</FONT>行,这说明至少有一个进程和所提供的<B normal"><FONT face="Times New Roman">pid</FONT></B>参数匹配——它不是僵进程,也没有被终止,因此它就不能被获取。在这种情况下,如果提供了<B normal"><FONT face="Times New Roman">WNOHANG</FONT></B>选项——这意味着如果不能获取子孙进程,那么调用者就不会等待——它向前跳转到最后,返回<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23411</FONT>:如果有信号量被接收,就退出并返回一个错误。这个信号量不是<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>——如果它是<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>,就应该已经发现了死亡的进程,因此就不可能执行到此处。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23413</FONT>:否则,一切都没有问题;调用者只需要等待一个子孙进程退出。因此,进程的状态被设置为<B normal"><FONT face="Times New Roman">TASK_INTERRUPTIBLE</FONT></B>并调用<B normal"><FONT face="Times New Roman">schedule</FONT></B>释放<FONT face="Times New Roman">CPU</FONT>给另一个进程使用。正在等待的进程直到再次获得<FONT face="Times New Roman">CPU</FONT>时才会返回,同时要再次检测死亡子孙进程(通过向回跳转到<FONT face="Times New Roman">23339</FONT>行的<B normal"><FONT face="Times New Roman">repeat</FONT></B>标号)。回想一下处于<B normal"><FONT face="Times New Roman">TASK_INTERRUPTIBLE</FONT></B>状态的进程要等待信号量将其唤醒——在这种情况下,它特别希望<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>来指明子孙进程已经退出了,但是任何信号量都可以到达。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23417</FONT>:<B normal"><FONT face="Times New Roman">flag</FONT></B>是<FONT face="Times New Roman">0</FONT>,因为或者进程没有子孙进程,或者所提供的<B normal"><FONT face="Times New Roman">pid</FONT></B>参数不能和它的任何子孙进程匹配——不管怎样,<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>都给调用者返回一个<B normal"><FONT face="Times New Roman">ECHILD</FONT></B>错误。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">release</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22951</FONT>:<B normal"><FONT face="Times New Roman">release</FONT></B>的唯一一个参数是指向要释放的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的指针。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22953</FONT>:确保该任务没有试图释放自身——这是会在内核中引起逻辑错误的一种无意义的情况。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22969</FONT>:<FONT face="Times New Roman">UP</FONT>代码实际上是通过调用<B normal"><FONT face="Times New Roman">free_uid</FONT></B>(<FONT face="Times New Roman">23532</FONT>行)开始的,它用来释放潜在共享的<B normal"><FONT face="Times New Roman">struct user_struct</FONT></B>结构,这个结构除了其它功能以外,还要帮助<B normal"><FONT face="Times New Roman">fork</FONT></B>确保不会出现单个用户影响所有进程的情况。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22970</FONT>:减小系统关于正在运行的任务总数的计数并释放<B normal"><FONT face="Times New Roman">tarray_freelist</FONT></B>中的僵死进程的时间片。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22974</FONT>:僵死进程的<FONT face="Times New Roman">PID</FONT>也会释放,并且使用<B normal"><FONT face="Times New Roman">REMOVE_LINKS</FONT></B>(<FONT face="Times New Roman">16876</FONT>行)解除它同进程表和任务列表的关联。注意,由于内核数据结构在此处正在做出修正,<B normal"><FONT face="Times New Roman">task</FONT></B>数组中的进程项并不需要被设置为<B normal"><FONT face="Times New Roman">NULL</FONT></B>;把它的空槽增加到自由列表中就足够了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22979</FONT>:僵死进程有关次要页表错误,主要页表错误的总数以及向外交换所使用的时间的数量被增加到当前进程对应的<FONT face="Times New Roman"> </FONT>“子孙进程计数”中——这是正确的;<B normal"><FONT face="Times New Roman">release</FONT></B>只能通过<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>调用,这样只允许进程释放自己的子孙进程。因此,当前进程必须是僵死进程的祖先。</P>22982:最后,应该回收垂死进程的<B normal">struct task_struct</B>结构,这可以通过对<B normal">free_task_struct</B>的调用(2391行)来实现。这个函数简单地回收存储在这个结构中的内存页。现在,进程最终功德圆满的寿终正寝了。
< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10126</FONT>:如果前面的工作可以很好地执行到此处,最后一步是要为新的可执行程序寻找一个二进制处理程序。如果<B normal"><FONT face="Times New Roman">search_binary_handler</FONT></B>成功找到了这种程序,整个过程就成功运行结束,并返回一个非负值以说明成功。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10134</FONT>:如果程序运行到了此处,那么前面的几步中肯定发生了错误。系统释放为新进程的参数和环境分配的所有页,接着必须返回一个负值通知调用者调用过程失败了。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">prepare_binprm</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9832</FONT>:<FONT face="Times New Roman"> <B normal">prepare_binprm</B></FONT>填写<B normal"><FONT face="Times New Roman">do_execve</FONT></B>的重要部分<B normal"><FONT face="Times New Roman">bprm</FONT></B>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9839</FONT>:<FONT face="Times New Roman"> </FONT>本行开始一些健全性检测,例如要确保执行的是文件而不是目录,并且文件的可执行位已经设置了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9858</FONT>:<FONT face="Times New Roman"> </FONT>如果已经被设置过<FONT face="Times New Roman">setuid</FONT>和<FONT face="Times New Roman">setuid</FONT>位,就根据它们的提示新进程应该把当前执行的用户作为一个不同的用户(如果<FONT face="Times New Roman">setuid</FONT>被置位)并且<FONT face="Times New Roman">/</FONT>或者把它作为一个不同组的成员(如果<FONT face="Times New Roman">setgid</FONT>被置位)。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9933</FONT>:<FONT face="Times New Roman"> </FONT>最后,<B normal"><FONT face="Times New Roman">prepare_binprm</FONT></B>从文件中读取前<FONT face="Times New Roman">128</FONT>个字节(而不是像该函数标题注释里说明的一样是前<FONT face="Times New Roman">512</FONT>个字节)到<B normal"><FONT face="Times New Roman">bprm</FONT></B>的<B normal"><FONT face="Times New Roman">buf</FONT></B>成员中。</P>< 0cm 0cm 0pt">顺便说一下,这里有一个延续已久的争论:在<FONT face="Times New Roman">13787</FONT>行,<B normal"><FONT face="Times New Roman">struct linux_binprm</FONT></B>结构的<FONT face="Times New Roman">buf</FONT>成员被声明为是<FONT face="Times New Roman">128</FONT>字节长,在<FONT face="Times New Roman">9933</FONT>行读入了<FONT face="Times New Roman">128</FONT>字节。但是字面上常量<FONT face="Times New Roman">128</FONT>用在两个地方——没有宏定义表示有必要保持两个数字的一致;因此,有可能会出现对其中一个进行改变而不改变相关的另一个的情况,这样就很可能摧毁系统。即使不从学术上考虑,这种忽略在保证效率的基础上是不能防止的——我不能想象出还有什么其它理由。</P>< 0cm 0cm 0pt">这是一个很好的对内核做点简短却有用的修改的机会:在每处这样使用<FONT face="Times New Roman">128</FONT>的地方都使用一个<B normal"><FONT face="Times New Roman">#define</FONT></B>语句(或者是使用类似于<B normal"><FONT face="Times New Roman">sizeof</FONT></B><B normal">(<FONT face="Times New Roman">bprm->buf</FONT></B><B normal">)</B>的语句)代替;存在几个其它实例,我会让你把它们都找到。如果你实验一下,你就会发现在这种情况下<B normal"><FONT face="Times New Roman">#define</FONT></B>为什么比<B normal"><FONT face="Times New Roman">sizeof</FONT></B>要好。(把这种重复出现的神奇数字加以定义和修正对于内核是更好的贡献。但是总体的修正工作要比看起来的困难,这只由于正确的对所有相关部分进行定位是很困难的;让我们一点一点地开始,最终会将其全部解决。)</P><H4 6pt 0cm; TEXT-INDENT: 0cm">search_binary_handler</H4>< 0cm 0cm 0pt">二进制处理程序是<FONT face="Times New Roman">Linux</FONT>内核统一处理各种二进制格式的机制,这是我们需要的,因为不是所有的文件都是以相同的文件格式存储的。一个很合适的例子是<FONT face="Times New Roman">Java</FONT>的<FONT face="Times New Roman">.class</FONT>文件。<FONT face="Times New Roman">Java</FONT>定义了一种平台无关的二进制可执行格式——无论它们是在什么平台上运行,它们的文件本身都是相同的——因此这些文件显然应该和<FONT face="Times New Roman">Linux</FONT>特有的可执行格式一样构建。通过使用适当的二进制处理程序,<FONT face="Times New Roman">Linux</FONT>可以把它们仿佛当作是自己特有的可执行文件一样处理。</P>< 0cm 0cm 0pt">后面我们会详细介绍二进制处理程序,但是现在你应该了解一些有关内容以便理解<B normal"><FONT face="Times New Roman">do_execve</FONT></B>是如何发现匹配的。它把这一工作交给<B normal"><FONT face="Times New Roman">search_binary_handler</FONT></B>(<FONT face="Times New Roman">9996</FONT>行)处理。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10037</FONT>:开始遍历处理内核的二进制处理程序链接列表,依次将<B normal"><FONT face="Times New Roman">bprm</FONT></B>传递给它们。(我们现在并不关心<B normal"><FONT face="Times New Roman">regs</FONT></B>参数。)更确切的说,二进制处理程序的链接列表的每一个元素都包含一组指向函数的指针,这些函数一起提供了对一种二进制格式的支持。(<FONT face="Times New Roman">13803</FONT>行定义的<B normal"><FONT face="Times New Roman">struct linux_binfmt</FONT></B>结构显示了其中包含的内容:我们感兴趣的部分是装载二进制的部分<B normal"><FONT face="Times New Roman">load_binary</FONT></B>;装载共享库的部分<B normal"><FONT face="Times New Roman">load_shlib</FONT></B>;创建内核转储映象的部分<B normal"><FONT face="Times New Roman">core_dump</FONT></B>。)<B normal"><FONT face="Times New Roman">search_binary_handler</FONT></B>简单调用每一个<B normal"><FONT face="Times New Roman">load_binary</FONT></B>函数,知道其中一个返回非负值指明它成功识别并装载了文件。<B normal"><FONT face="Times New Roman">search_binary_handler</FONT></B>返回负值指明发生的错误,其中包括不能找到匹配的二进制处理程序的错误。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10070</FONT>:如果<FONT face="Times New Roman">10037</FONT>行开始的循环不能找到匹配的二进制处理程序,本行就试图装载新的二进制格式,它会引起第二次尝试,并应该取得成功。因此整个操作被包含在从<FONT face="Times New Roman">10036</FONT>行开始的两次执行的循环中。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">可执行格式</H2>< 0cm 0cm 0pt">正如前面一节中说明的一样,不是所有程序都使用相同的文件格式存储,<FONT face="Times New Roman">Linux</FONT>使用二进制处理程序把它们之间的区别掩盖掉了。</P>< 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>当前“本地的”可执行格式(如果“本地”在系统中可以给各种格式提供良好支持)是可执行链接格式(<FONT face="Times New Roman">ELF</FONT>)。<FONT face="Times New Roman">ELF</FONT>只是全部替换了原来的称为<FONT face="Times New Roman">a.out</FONT>的格式,替换之前的格式很难说是灵活的——除了有一些其它缺点以外,<FONT face="Times New Roman">a.out</FONT>还很难适用于动态链接,这会使得共享库难于实现。<FONT face="Times New Roman">Linux</FONT>仍然为<FONT face="Times New Roman">a.out</FONT>保留了一个二进制处理程序,但通常是使用<FONT face="Times New Roman">ELF</FONT>。</P>< 0cm 0cm 0pt">二进制处理程序通过某种内嵌在文件开头的“<FONT face="Times New Roman">magic</FONT>序列”(一个特殊字节序列)来识别文件,有时也会通过文件名的一些特性。例如,你会看到的<FONT face="Times New Roman">Java</FONT>处理程序可以保证文件名以<FONT face="Times New Roman">.class</FONT>结尾并且前四个字节是(以十六进制)<FONT face="Times New Roman">0xcafebabe</FONT>,这是<FONT face="Times New Roman">Java</FONT>标准所定义的。</P>< 0cm 0cm 0pt">下面是<FONT face="Times New Roman">2.2</FONT>版本内核所提供的二进制处理程序(这是在我的<FONT face="Times New Roman">Intel</FONT>系统中的;<FONT face="Times New Roman">Linux</FONT>的其它平台的移植移植版本,例如<FONT face="Times New Roman">owerPC</FONT>和<FONT face="Times New Roman">SPARC</FONT>上,需要使用其它的处理程序):</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l <FONT face="Times New Roman">a.out</FONT>(在文件<FONT face="Times New Roman">fs/binfmt_aout.c</FONT>中)——这是为了支持原来风格的<FONT face="Times New Roman">Linux</FONT>二进制文件。这仍然是为了满足一些系统的向后兼容的需要,但是基本上<FONT face="Times New Roman">a.out</FONT>很快就会光荣退役了。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l <FONT face="Times New Roman">EFL</FONT>(在文件<FONT face="Times New Roman">fs/binfmt_elf.c</FONT>中)——只是为了支持现在新风格的<FONT face="Times New Roman">Linux</FONT>二进制文件。这在可执行文件和共享库中都广泛使用。最新的<FONT face="Times New Roman">Linux</FONT>系统(例如<FONT face="Times New Roman">Red Hat 5.2</FONT>)一般只预装了<FONT face="Times New Roman">ELF</FONT>二进制文件,但是特殊情况下如果你决定装载<FONT face="Times New Roman">a.out</FONT>二进制文件,那么系统也可以对它提供支持。注意即使<FONT face="Times New Roman">ELF</FONT>被作为惯用的<FONT face="Times New Roman">Linux</FONT>本地格式,也要和其它格式一样使用二进制处理程序——内核并没有特殊的偏好。避免特殊情况的惯例能够简化内核代码。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l <FONT face="Times New Roman">EM86</FONT>(在文件<FONT face="Times New Roman">fs/binfmt_em86.c</FONT>中)——帮你在<FONT face="Times New Roman">Alpha</FONT>机器上运行<FONT face="Times New Roman">Intel</FONT>的<FONT face="Times New Roman">Linux</FONT>二进制文件,仿佛它们就是<FONT face="Times New Roman">Alpha</FONT>的本地二进制文件。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l <FONT face="Times New Roman">Java</FONT>(在文件<FONT face="Times New Roman">fs/binfmt_java.c</FONT>中)——使你可以不必每次都麻烦地定义<FONT face="Times New Roman">Java</FONT>字节码的解释程序就可以执行<FONT face="Times New Roman">Java</FONT>的<FONT face="Times New Roman">.class</FONT>文件。这种机制和脚本中使用的机制类似;通过把<FONT face="Times New Roman">.class</FONT>文件的文件名作为参数传递,处理程序返回来为你整型字节码处理程序。从用户的观点来看,<FONT face="Times New Roman">Java</FONT>二进制文件是作为本地可执行文件处理的。在本章的后面内容中我们会详细介绍这个处理程序。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l <FONT face="Times New Roman">Misc</FONT>(在文件<FONT face="Times New Roman">fs/binfmt_misc.c</FONT>中)——这是最明智地使用二进制处理程序的方法,这个处理程序通过内嵌的特征数字或者文件名后缀可以识别出各种二进制格式——但是其最优秀的特性是它在运行期可以配置,而不是只能在编译器可以配置。因此,遵守这些限制,你就可以快速的增加对新二进制文件的支持,而不用重新编译内核,也无须重新启动机器。(这实在太棒了!)源程序文件中的注释建议最终使用它来取代<FONT face="Times New Roman">Java</FONT>和<FONT face="Times New Roman">M86</FONT>二进制处理程序。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l 脚本(在文件<FONT face="Times New Roman">fs/binfmt_script.c</FONT>中)——对于<FONT face="Times New Roman">shell</FONT>脚本,<FONT face="Times New Roman">erl</FONT>脚本等提供支持。宽松一点地说,所有前面两个字符是<FONT face="Times New Roman">#!</FONT>的可执行文件都规由这个二进制处理程序进行处理。</P>< 0cm 0cm 0pt">在上面这些二进制处理程序中,本书中只对<FONT face="Times New Roman">Java</FONT>和<FONT face="Times New Roman">ELF</FONT>处理程序进行了说明(分别从<FONT face="Times New Roman">9083</FONT>行和<FONT face="Times New Roman">7656</FONT>行开始),因为作为我们关心的基本内容,我们更关心内核如何处理各种不同格式间的区别,而不是每一种单个二进制处理程序的细节(虽然它自己也是一个很有趣的主题)。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">一个例子:Java二进制处理程序</H2>< 0cm 0cm 0pt">如同前面你看到的一样,<B normal"><FONT face="Times New Roman">do_execve</FONT></B>遍历一个代表二进制处理程序的<B normal"><FONT face="Times New Roman">struct linux_binfmt</FONT></B>结构的链接列表,调用每个结构的<B normal"><FONT face="Times New Roman">load_binary</FONT></B>成员指向的函数直到其中一个成功(当然也或者到已经试验完了所有的格式为止)。但是这些结构又从何而来呢?函数<B normal"><FONT face="Times New Roman">load_binary</FONT></B>是如何实现的?为了寻找这些答案,让我们来看一下<FONT face="Times New Roman">fs/binfmt_java.c</FONT>文件。</P>< 0cm 0cm 0pt">这个模块处理一些不是涉及在<FONT face="Times New Roman">Web</FONT>浏览器上使用<B normal"><FONT face="Times New Roman">java_format</FONT></B>(<FONT face="Times New Roman">9236</FONT>行)执行的<FONT face="Times New Roman">Java</FONT>程序的<FONT face="Times New Roman">Java</FONT>二进制文件和相关的函数。它使用<B normal"><FONT face="Times New Roman">applet_format</FONT></B>(<FONT face="Times New Roman">9254</FONT>行)及相关函数处理<FONT face="Times New Roman">Java</FONT>小程序(<FONT face="Times New Roman">Applet</FONT>)。在本节剩余部分的内容中,我们会集中看一下对于非<FONT face="Times New Roman">Java</FONT>小程序的支持;对于<FONT face="Times New Roman">Java</FONT>小程序的支持实际上是相同的。</P>< 0cm 0cm 0pt">如果重写<FONT face="Times New Roman">fs/binfmt_java.c</FONT>中的函数用来加强<FONT face="Times New Roman">Java</FONT>小程序函数和非<FONT face="Times New Roman">Java</FONT>小程序函数之间的相同代码的数量就更好了。虽然它注定最终要被“<FONT face="Times New Roman">misc</FONT>”二进制处理程序取代,但是现在还只是在讨论,尚未实行。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">do_load_java</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9108</FONT>:<FONT face="Times New Roman"> </FONT>这是实际处理装载<FONT face="Times New Roman">Java</FONT>的<FONT face="Times New Roman">.class</FONT>文件工作的函数。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9117</FONT>:<FONT face="Times New Roman"> </FONT>通过检测特征数字<FONT face="Times New Roman">0xcafebabe</FONT>开始,这是因为<FONT face="Times New Roman">Java</FONT>标准规定所有有效的类文件都使用这个字符序列开始。接着开始执行健全性检测,一直到<FONT face="Times New Roman">9147</FONT>行,确保没有递归调用而且正在请求执行的可执行文件是以<FONT face="Times New Roman">.class</FONT>结尾的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9148</FONT>:<FONT face="Times New Roman"> </FONT>此处,所有的健全性检测已经通过了。现在,<B normal"><FONT face="Times New Roman">do_load_java</FONT></B>取得文件的基本名字,将其和<FONT face="Times New Roman">Java</FONT>字节码解释程序一起放置到程序空间中,并试图执行<FONT face="Times New Roman">Java</FONT>字节码解释程序。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9165</FONT>:<FONT face="Times New Roman"> </FONT>使用我们在<B normal"><FONT face="Times New Roman">do_execve</FONT></B>中见到的同一个进程执行解释程序。特殊情况下,就像查询<B normal"><FONT face="Times New Roman">do_load_java</FONT></B>的方法一样,使用<B normal"><FONT face="Times New Roman">search_binary_handler</FONT></B>为解释程序查询二进制处理程序。(实际上,虽然它不一定非要是<FONT face="Times New Roman">ELF</FONT>二进制文件,但是它也可能是。)</P><P 0cm 0cm 0pt">记住其它处理程序不会分配新的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构——我们在使用<B normal"><FONT face="Times New Roman">fork</FONT></B>的时候也碰到了这个问题。其它处理程序只是修改现存进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构。如果你希望细致地了解这是如何实现的,你的入手点应该是<B normal"><FONT face="Times New Roman">do_load_elf_binary</FONT></B>(<FONT face="Times New Roman">8072</FONT>行)——我们关心的部分从<FONT face="Times New Roman">8273</FONT>行开始。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">load_java</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9226</FONT>:<B normal"><FONT face="Times New Roman">load_java</FONT></B>是其它外部对象装载<FONT face="Times New Roman">.class</FONT>文件时所使用的函数。它首先递增内核模块使用的计数(如果作为内核模块编译),随后又将其递减,但是实际的工作是由<B normal"><FONT face="Times New Roman">do_load_java</FONT></B>(<FONT face="Times New Roman">9108</FONT>行)处理的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">java_format</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9236</FONT>:<FONT face="Times New Roman"> </FONT>通过比较<B normal"><FONT face="Times New Roman">java_format</FONT></B>的初始化和<B normal"><FONT face="Times New Roman">struct linux_binfmt</FONT></B>结构(<FONT face="Times New Roman">13803</FONT>行)的定义,你可以看出这个模块没有提供对共享库和内核卸载的支持,只提供了对装载可执行程序的支持;而且这种支持是通过<B normal"><FONT face="Times New Roman">load_java</FONT></B>函数实现的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">init_java_binfmt</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9262</FONT>:<FONT face="Times New Roman"> </FONT>指向这个模块的项是<B normal"><FONT face="Times New Roman">init_java_binfmt</FONT></B>,它把两个静态<B normal"><FONT face="Times New Roman">struct linux_binfmt</FONT></B>结构<B normal"><FONT face="Times New Roman">java_format</FONT></B>和<B normal"><FONT face="Times New Roman">applet_format</FONT></B>的地址压入系统列表中。如果对<FONT face="Times New Roman">Java</FONT>二进制文件的支持被编译进了内核,就在<FONT face="Times New Roman">9355</FONT>行调用<B normal"><FONT face="Times New Roman">init_java_binfmt</FONT></B>,或者如果<FONT face="Times New Roman">Java</FONT>二进制文件的支持被作为一个内核模块编译进了内核,就使用<FONT face="Times New Roman">kmod</FONT>任务。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">调度:了解它们是如何运行的!</H2><P 0cm 0cm 0pt">在应用程序被装载以后,必须获得对<FONT face="Times New Roman">CPU</FONT>的访问。这是调度程序涉及的领域。操作系统调度程序基本上划分为两类:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 42.25pt">l 复杂调度程序——运行需要花费相当长的时间,但是希望可以全面提高系统性能。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 42.25pt">l 快餐式(<FONT face="Times New Roman">quick-and-dirty</FONT>)调度程序——只是试图处理一些尽量简单的合理的工作就退出,从而进程本身将可以尽可能多的获得<FONT face="Times New Roman">CPU</FONT>。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>调度程序是后面一种情况。不要把“<FONT face="Times New Roman">quick-and-dirty</FONT>”解释成贬义的词,虽然实际的情况是:<FONT face="Times New Roman">Linux</FONT>的调度程序在商业和自由领域中都从根本上痛击了其竞争者。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">调度函数和调度策略</H2><P 0cm 0cm 0pt">内核主要的调度函数经过仔细挑选使用<B normal"><FONT face="Times New Roman">schedule</FONT></B>这个名字,该函数从<FONT face="Times New Roman">26686</FONT>行开始。这实际上是个很简单的函数,比它看起来还要简单,虽然由于它把三种调度策略合成了一种而其意义显得有些不是很明显。而且对于<FONT face="Times New Roman">SMP</FONT>的支持也增加了一定的复杂性,这一点将在第<FONT face="Times New Roman">10</FONT>章中详细讨论。</P><P 0cm 0cm 0pt">通常情况下使用的调度策略和进程有关。给定进程使用的调度算法称为调度策略,这在进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">policy</FONT></B>成员中有所反映。普通情况下,<B normal"><FONT face="Times New Roman">policy</FONT></B>是<B normal"><FONT face="Times New Roman">SCHED_OTHER</FONT></B><B normal">、<FONT face="Times New Roman">SCHED_FIFO</FONT></B>,或者<B normal"><FONT face="Times New Roman">SCHED_RR</FONT></B>其中一个的位集。但是它也可能含有<B normal"><FONT face="Times New Roman">SCHED_YIED</FONT></B>位集,如果进程决定交出<FONT face="Times New Roman">CPU</FONT>——例如,通过调用<B normal"><FONT face="Times New Roman">sched_yield</FONT></B>系统调用(请参看<B normal"><FONT face="Times New Roman">sched_yield</FONT></B>,<FONT face="Times New Roman">27757</FONT>行)。</P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">SCHED_XXX</FONT></B>常量在<FONT face="Times New Roman">16196</FONT>行到<FONT face="Times New Roman">16202</FONT>行宏定义。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">16196</FONT>:<B normal"><FONT face="Times New Roman">SCHED_OTHER</FONT></B>意味着传统<FONT face="Times New Roman">Unix</FONT>调度是使用它的——这不是一个实时进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16197</FONT>:<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>意味着这是一个实时进程,这要遵守<FONT face="Times New Roman">POSIX.1b</FONT>标准的<FONT face="Times New Roman">FIFO</FONT>(先进先出)调度程序。它会一直运行,直到有一个进程在<FONT face="Times New Roman">I/O</FONT>阻塞,因而明确释放<FONT face="Times New Roman">CPU</FONT>,或者是<FONT face="Times New Roman">CPU</FONT>被另一个具有更高<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>的实时进程抢占了。在<FONT face="Times New Roman">Linux</FONT>实现中,<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>进程拥有时间片——只有当时间片结束时它们才被迫释放<FONT face="Times New Roman">CPU</FONT>。因此,如同<FONT face="Times New Roman">POSIX.1b</FONT>中规定一样,这样的进程就像没有时间片一样运行。因此进程要保持对其时间片进行记录的这一事实主要是为了实现的方便,因此我们就不必使用<B normal"><FONT face="Times New Roman">if(!(current->policy & SCHED_FIFO)) { … }</FONT></B>来弄乱这些代码。还有,这样处理速度可能会快一些——其它实际可行的策略都需要记录时间片,并持续检测是否我们需要记录时间片会比简单的跟踪它速度更慢。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16198</FONT>:<B normal"><FONT face="Times New Roman">SCHED_RR</FONT></B>意味着这是一个实时进程,要遵守<FONT face="Times New Roman">POSIX.1b</FONT>的<FONT face="Times New Roman">RR</FONT>(循环:<FONT face="Times New Roman">round-robin</FONT>)调度规则。除了时间片有些不同之外,这和<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>类似。当<B normal"><FONT face="Times New Roman">SCHED_RR</FONT></B>进程的时间片用完后,就使用相同的<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>跳转到<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>和<B normal"><FONT face="Times New Roman">SCHED_RR</FONT></B>列表的最后。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16202</FONT>:<B normal"><FONT face="Times New Roman">SCHED_YIELD</FONT></B>并不是一种调度策略,而是截取调度策略的一个附加位。如同前面说明的一样,如果有其它进程需要<FONT face="Times New Roman">CPU</FONT>,它就提示调度程序释放<FONT face="Times New Roman">CPU</FONT>。特别要注意的是这甚至会引起实时进程把<FONT face="Times New Roman">CPU</FONT>释放给非实时进程。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">schedule</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26689</FONT>:<B normal"><FONT face="Times New Roman">prev</FONT></B>和<B normal"><FONT face="Times New Roman">next</FONT></B>会被设置为<B normal"><FONT face="Times New Roman">schedule</FONT></B>最感兴趣的两个进程:其中一个是在调用<B normal"><FONT face="Times New Roman">schedule</FONT></B>时正在运行的进程(<B normal"><FONT face="Times New Roman">prev</FONT></B>),另外一个应该是接着就给予<FONT face="Times New Roman">CPU</FONT>的进程(<B normal"><FONT face="Times New Roman">next</FONT></B>)。记住<B normal"><FONT face="Times New Roman">prev</FONT></B>和<B normal"><FONT face="Times New Roman">next</FONT></B>可能是相同的——<B normal"><FONT face="Times New Roman">schedule</FONT></B>可以重新调度已经获得<FONT face="Times New Roman">CPU</FONT>的进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26706</FONT>:如同第<FONT face="Times New Roman">6</FONT>章中介绍的一样,这就是中断处理程序的“下半部分”运行的地方。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26715</FONT>:内核实时系统部分的实现,循环调度程序(<B normal"><FONT face="Times New Roman">SCHED_RR</FONT></B>)通过移动“耗尽的”<FONT face="Times New Roman">RR</FONT>进程——已经用完其时间片的进程——到队列末尾,这样具有相同优先级的其它<FONT face="Times New Roman">RR</FONT>进程就可以获得时间片了。同时这补充了耗尽进程的时间片。重要的是它并不是为<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>这样处理的,这样和预计的一样,后面的进程在其时间片偶然用完时就无须释放<FONT face="Times New Roman">CPU</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26720</FONT>:由于代码的其它部分已经决定了进程必须被移进或移出<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>状态,所以会经常使用<B normal"><FONT face="Times New Roman">schedule</FONT></B>——例如,如果进程正在等待的硬件条件已经发生了——所以如果必要,这个<B normal"><FONT face="Times New Roman">switch</FONT></B>会改变进程的状态。如果进程已经处于<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>状态,它就无须处理了。如果它是可以中断的(等待信号量)并且信号量到达了进程,就返回<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>状态。在所有其它情况下(例如,进程已经处于<B normal"><FONT face="Times New Roman">TASK_UNINTERUPTIBLE</FONT></B>状态了),应该从运行队列中将进程移走。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26735</FONT>:将<B normal"><FONT face="Times New Roman">p</FONT></B>初始化为运行队列中的第一个任务;<B normal"><FONT face="Times New Roman">p</FONT></B>会遍历队列中的所有任务。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26736</FONT>:<FONT face="Times New Roman">c</FONT>记录了运行队列中所有进程的最好“<FONT face="Times New Roman">goodness</FONT>”——具有最好“<FONT face="Times New Roman">goodness</FONT>”的进程是最易获得<FONT face="Times New Roman">CPU</FONT>的进程。(我们很快就会讨论<B normal"><FONT face="Times New Roman">goodness</FONT></B>。)<FONT face="Times New Roman">goodness</FONT>值越高越好,一个进程的<FONT face="Times New Roman">goodness</FONT>值永远不会为负——这是<FONT face="Times New Roman">Unix</FONT>用户经常见到的一种奇异情况,其中较高的优先级(通常称为较高“<FONT face="Times New Roman">niceness</FONT>”级)意味着进程会较少地获得<FONT face="Times New Roman">CPU</FONT>时间。(至少这在内核中是有意义的。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26757</FONT>:开始遍历执行任务列表,跟踪具有最好<FONT face="Times New Roman">goodness</FONT>的进程。注意只有在当前记录被破坏而不是当它简单地被约束时它才会改变最好进程的概念。因此,出于对队列中第一个进程的原因,这种约束就会被打破了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26758</FONT>:这个循环中只考虑了唯一一个可以调度的进程。<B normal"><FONT face="Times New Roman">can_schedule</FONT></B>宏的<FONT face="Times New Roman">SMP</FONT>版本在<FONT face="Times New Roman">26568</FONT>行定义;其定义使<FONT face="Times New Roman">SMP</FONT>内核只有任务尚未在<FONT face="Times New Roman">CPU</FONT>上运行才会把调度作为该<FONT face="Times New Roman">CPU</FONT>上的一个任务。(这样具有完美的意义——在几乎不必要的任务中造成混淆完全是一种浪费。)<FONT face="Times New Roman">UP</FONT>版本在<FONT face="Times New Roman">26573</FONT>行,它总是真值——换而言之,在<FONT face="Times New Roman">UP</FONT>的情况下,运行队列中的每一个进程都需要竞争<FONT face="Times New Roman">CPU</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26767</FONT>:值为<FONT face="Times New Roman">0</FONT>的<FONT face="Times New Roman">goodness</FONT>意味着进程已经用完它的时间片或者它已经明确说明要释放<FONT face="Times New Roman">CPU</FONT>。如果所有运行队列中的所有进程都具有<FONT face="Times New Roman">0</FONT>值的<FONT face="Times New Roman">goodness</FONT>,在循环结束后<B normal"><FONT face="Times New Roman">c</FONT></B>的值就是<FONT face="Times New Roman">0</FONT>。在这种情况下,<B normal"><FONT face="Times New Roman">schedule</FONT></B>要重新计算进程计数器;新计数器的值是原来值的一半加上进程的静态优先级——由于除非进程已经释放<FONT face="Times New Roman">CPU</FONT>,否则原来计数器的值都是<FONT face="Times New Roman">0</FONT>,<B normal"><FONT face="Times New Roman">schedule</FONT></B>通常只是把计数器重新初始化为静态优先级。(中断处理程序和由另外一个处理器引起的分支在<B normal"><FONT face="Times New Roman">schedule</FONT></B>搜寻<FONT face="Times New Roman">goodness</FONT>最大值时都将增加此循环中的计数器,因此由于这个原因计数器可能不会为<FONT face="Times New Roman">0</FONT>。虽然这有些罕见。)调度程序不必麻烦地重新计算现在哪一个进程具有最高的<FONT face="Times New Roman">goodness</FONT>值;它只是调度前面循环中遇到的第一个进程。此时,这个进程是它发现的第一个具有次高<FONT face="Times New Roman">goodness</FONT>值(<FONT face="Times New Roman">0</FONT>)的进程,因此<B normal"><FONT face="Times New Roman">schedule</FONT></B>就能够计算出自己现在和以后所应该运行的任务。(记住,这就是“<FONT face="Times New Roman">quick-and-dirty</FONT>”的思想。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26801</FONT>:如果<B normal"><FONT face="Times New Roman">schedule</FONT></B>已经选择了一个不同于前面正在运行的进程来调度,那么它就必须挂起原来的进程并允许新的进程运行。这是通过后面我们将介绍的<B normal"><FONT face="Times New Roman">switch_to</FONT></B>处理的。<B normal"><FONT face="Times New Roman">switch_to</FONT></B>的一个重要结果对于应用程序开发者来说可能显得有些奇怪:对于<B normal"><FONT face="Times New Roman">schedule</FONT></B>的调用并不返回。也就是它不是立即返回的;在系统条件判断语句返回到当前任务时调用就会返回。作为一个特殊情况,当任务退出而调用<B normal"><FONT face="Times New Roman">schedule</FONT></B>时,对于<B normal"><FONT face="Times New Roman">schedule</FONT></B>的调用从不会返回——因为内核不会返回已经退出的任务。还有另外一种特殊情况,如果<B normal"><FONT face="Times New Roman">schedule</FONT></B>不会调度其它进程——也就是说,如果在<B normal"><FONT face="Times New Roman">schedule</FONT></B>结束时<B normal"><FONT face="Times New Roman">next</FONT></B>和<B normal"><FONT face="Times New Roman">prev</FONT></B>是相同的——那么上下文中的跳转不会执行,<B normal"><FONT face="Times New Roman">schedule</FONT></B>实际上不会立即返回。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26809</FONT>:<B normal"><FONT face="Times New Roman">schedule</FONT></B>末尾的<B normal"><FONT face="Times New Roman">__schedule_tail</FONT></B>和<B normal"><FONT face="Times New Roman">reacquire_kernel_lock</FONT></B>函数在<FONT face="Times New Roman">UP</FONT>平台上不执行任何操作,因此现在我们就已经看完了调度程序的内核。顺便说一下,为了确保你已经正确的理解了这些代码,自己证明下面的性质:如果运行队列为空,那么下面就会调用<FONT face="Times New Roman">idle</FONT>任务。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">switch_to</H4><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">switch_to</FONT></B>处理从一个进程到下一个进程的跳转,称为上下文跳转(<FONT face="Times New Roman">context-switching</FONT>);这是在不同处理器上会不同处理之间进行的低级特性。有趣的是,在<FONT face="Times New Roman">x86</FONT>平台上内核开发人员使用软件处理大多数的上下文跳转,这样就忽略了一些硬件的支持。这种机制背后的原因在<B normal"><FONT face="Times New Roman">__switch_to</FONT></B>函数(<FONT face="Times New Roman">2638</FONT>行)上面的标题注释中有所说明,这个函数和<B normal"><FONT face="Times New Roman">switch_to</FONT></B>宏(<FONT face="Times New Roman">12939</FONT>行)一起处理上下文跳转。</P><P 0cm 0cm 0pt">由于很多上下文跳转要依赖于对内核处理内存方式的正确理解,这在下一章中才会详细介绍,本章只是稍微涉及一点。上下文跳转背后的基本思想是记忆当前位置和将要到达的位置——这是我们必须保存的当前上下文——接着跳转到另外一个前面已经存储过了的上下文。通过使用一部分汇编代码,<B normal"><FONT face="Times New Roman">switch_to</FONT></B>宏保存了后面将要介绍的上下文的两个重要的部分。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12945</FONT>:首先,<B normal"><FONT face="Times New Roman">switch_to</FONT></B>宏保存<FONT face="Times New Roman">ESP</FONT>寄存器的内容,它指向进程的当前堆栈。堆栈在下一章中将深入介绍;现在你只需要简单了解堆栈中保存的局部变量和函数调用信息。<B normal"><FONT face="Times New Roman">switch_to</FONT></B>宏也保存<FONT face="Times New Roman">EIP</FONT>寄存器的内容,这是进程的当前指令指针——如果允许继续运行时所执行的为下一条指令的地址。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12948</FONT>:把<B normal"><FONT face="Times New Roman">next->tss.eip</FONT></B>——保存指令的指针——压入返回堆栈,记录当后面紧跟的跳转到<B normal"><FONT face="Times New Roman">__switch_to</FONT></B>的<B normal"><FONT face="Times New Roman">jmp</FONT></B>返回时的返回地址。这样做的最终结果是当<B normal"><FONT face="Times New Roman">__switch_to</FONT></B>返回时,我们又回到了新的进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12949</FONT>:调用<B normal"><FONT face="Times New Roman">__switch_to</FONT></B>(<FONT face="Times New Roman">2638</FONT>行),它完成段寄存器和页表的保存和恢复工作。在你阅读完第<FONT face="Times New Roman">8</FONT>章以后这些特征数字就更有意义了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12955</FONT>:<B normal"><FONT face="Times New Roman">tss</FONT></B>代表<I normal"><FONT face="Times New Roman">task-state</FONT></I>段,这是<FONT face="Times New Roman">Intel</FONT>使用的支持硬件上下文跳转的<FONT face="Times New Roman">CPU</FONT>特性的术语。虽然内核代码使用软件实现上下文跳转,但是开发人员仍然会使用<FONT face="Times New Roman">TSS</FONT>来记录进程的状态。<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">tss</FONT></B>成员的类型是<B normal"><FONT face="Times New Roman">struct thread_struct</FONT></B>结构,本书中为了节省空间,忽略了它的定义。其成员仅仅对应于<FONT face="Times New Roman">x86</FONT>的<FONT face="Times New Roman">TSS</FONT>——成员是为<FONT face="Times New Roman">EIP</FONT>和<FONT face="Times New Roman">ESP</FONT>而存在的,如此而已。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">计算goodness值</H2><P 0cm 0cm 0pt">进程的<FONT face="Times New Roman">goodness</FONT>值通过<B normal"><FONT face="Times New Roman">goodness</FONT></B>函数(<FONT face="Times New Roman">26388</FONT>行)计算。<B normal"><FONT face="Times New Roman">goodness</FONT></B>返回下面两类中的一个值:<FONT face="Times New Roman">1,000</FONT>以下或者<FONT face="Times New Roman">1,000</FONT>以上。<FONT face="Times New Roman">1,000</FONT>和<FONT face="Times New Roman">1,000</FONT>以上的值只能赋给“实时”进程,从<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">999</FONT>的值只能赋给“普通”进程。实际上普通进程的<FONT face="Times New Roman">goodness</FONT>值只使用了这个范围底部的一部分,从<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">41</FONT>(或者对于<FONT face="Times New Roman">SMP</FONT>来说是<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">56</FONT>,因为<FONT face="Times New Roman">SMP</FONT>模式会优先照顾等待同一个处理器的进程)。无论是在<FONT face="Times New Roman">SMP</FONT>还是在<FONT face="Times New Roman">UP</FONT>上,实时进程的<FONT face="Times New Roman">goodness</FONT>值的范围都是从<FONT face="Times New Roman">1,001</FONT>到<FONT face="Times New Roman">1,099</FONT>。</P><P 0cm 0cm 0pt">有关这两类<FONT face="Times New Roman">goodness</FONT>结果的重要的一点是该值在实时系统中的范围肯定会比非实时系统的范围要高(因此偏移量(<FONT face="Times New Roman">offset</FONT>)是<FONT face="Times New Roman">100</FONT>而不是<FONT face="Times New Roman">1000</FONT>)。<FONT face="Times New Roman">POSIX..1b</FONT>规定内核要确保在实时进程和非实时进程同时竞争<FONT face="Times New Roman">CPU</FONT>时,实时进程要优先于非实时进程。由于调度程序总是选择具有最大<FONT face="Times New Roman">goodness</FONT>值的进程,又由于任何尚未释放<FONT face="Times New Roman">CPU</FONT>的实时进程的<FONT face="Times New Roman">goodness</FONT>值总是比非实时进程的<FONT face="Times New Roman">goodness</FONT>大,<FONT face="Times New Roman">Linux</FONT>对这一点的遵守是很容易得到证明的。</P><P 0cm 0cm 0pt">尽管在<B normal"><FONT face="Times New Roman">goodness</FONT></B>上面的标题注释中有所说明,该函数还是从不会返回<FONT face="Times New Roman">-1,000</FONT>的,也不会返回其它的负值。由于<FONT face="Times New Roman">idle</FONT>进程的<B normal"><FONT face="Times New Roman">counter</FONT></B>值为负,所以如果使用<FONT face="Times New Roman">idle</FONT>进程作为参数调用<B normal"><FONT face="Times New Roman">goodness</FONT></B>,就会返回负值,但这是不会发生的。</P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">goodness</FONT></B>只是一个简单的函数,但是它是<FONT face="Times New Roman">Linux</FONT>调度程序必不可少的部分。运行对立中的每个进程每次执行<B normal"><FONT face="Times New Roman">schedule</FONT></B>时都可能调用它,因此其执行速度必须很快。但是如果一旦它调度失误,那么整个系统都要遭殃了。考虑到这些冲突压力,我想改进现有的系统是相当困难的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">goodness</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26394</FONT>:如果进程已经释放了<FONT face="Times New Roman">CPU</FONT>,就返回<FONT face="Times New Roman">0</FONT>(在清除<B normal"><FONT face="Times New Roman">SCHED_YIELD</FONT></B>位之后,这是因为进程只可能有一次想释放<FONT face="Times New Roman">CPU</FONT>,现在它已经的确把<FONT face="Times New Roman">CPU</FONT>释放了)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26402</FONT>:如果这是一个实时进程,<B normal"><FONT face="Times New Roman">goodness</FONT></B>返回的值就属于数值较高的一类;这要精确地依赖于<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>的值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26411</FONT>:此处,代码识别出这是一个非实时进程,它把<FONT face="Times New Roman">goodness</FONT>(在这个函数中被称为<B normal"><FONT face="Times New Roman">weight</FONT></B>)初始化为其当前的<B normal"><FONT face="Times New Roman">counter</FONT></B>值,这样如果进程已经占用<FONT face="Times New Roman">CPU</FONT>一段时间了,或者进程开始的优先级比较低,那么进程就不太可能获得<FONT face="Times New Roman">CPU</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26412</FONT>:如果权值<B normal"><FONT face="Times New Roman">weight</FONT></B>的值为<FONT face="Times New Roman">0</FONT>,那么进程的计数器就已经被用完了,因此<B normal"><FONT face="Times New Roman">goodness</FONT></B>就不会再增加加权因素。其它进程就可以有机会运行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26418</FONT>:尽力优先考虑等待同一个处理器的进程(只在<FONT face="Times New Roman">SMP</FONT>系统中是这样——顺便说一下,考虑一下运行在一个双处理器的系统中的三个进程的实现情况)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26423</FONT>:给相关的当前进程或者当前线程增加了一些优点;这有助于合理使用缓存以避免使用昂贵的<FONT face="Times New Roman">MMU</FONT>上下文跳转。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26425</FONT>:增加进程的<B normal"><FONT face="Times New Roman">priority</FONT></B>。这样,<B normal"><FONT face="Times New Roman">goodness</FONT></B>(和其它类似的调度程序)就对较高优先级的进程比对较低优先级的进程更感兴趣,即使在前面进程已经部分用完了它们的时间片也是这样。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26428</FONT>:返回计算出来的<FONT face="Times New Roman">goodness</FONT>值。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">非实时优先级</H2><P 0cm 0cm 0pt">每个<FONT face="Times New Roman">Linux</FONT>进程都有一个优先级,这是从<FONT face="Times New Roman">1</FONT>到<FONT face="Times New Roman">40</FONT>的一个整数,其值存储在<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">priority</FONT></B>成员中。(对于实时进程,在<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构中还会使用一个成员——<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>成员。随后很快就会对它进行更详细的讨论。)它的范围使用<B normal"><FONT face="Times New Roman">PRIO_MIN</FONT></B>(在<FONT face="Times New Roman">16094</FONT>行宏定义为<FONT face="Times New Roman">-20</FONT>)和<B normal"><FONT face="Times New Roman">PRIO_MAX</FONT></B>(在<FONT face="Times New Roman">16095</FONT>行宏定义为<FONT face="Times New Roman">20</FONT>)限定——理论上来说,的确是这样。但是非常令人气恼的是,控制优先级的函数——<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>和<B normal"><FONT face="Times New Roman">sys_nice</FONT></B>——并没有注意到这些明显的常量,却相反宁愿使用一些固定的值。(它们也使用最大的完美值<FONT face="Times New Roman">19</FONT>,而不是<FONT face="Times New Roman">20</FONT>。)基于这个原因,<B normal"><FONT face="Times New Roman">PRIO_MIN</FONT></B>和<B normal"><FONT face="Times New Roman">PRIO_MAX</FONT></B>两个常量并没有广泛使用。不过这又是一个热心读者改进代码的机会。</P><P 0cm 0cm 0pt">由于已经在文档中说明<B normal"><FONT face="Times New Roman">sys_nice</FONT></B>(<FONT face="Times New Roman">27562</FONT>行)为要废弃不用了——可能会使用<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>来重新实现——我们就忽略前面一个函数,只讨论后面一个。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_setpriority</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29213</FONT>:<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>使用三个参数——<B normal"><FONT face="Times New Roman">which</FONT></B>,<B normal"><FONT face="Times New Roman">who</FONT></B>和<B normal"><FONT face="Times New Roman">niceval</FONT></B>。<B normal"><FONT face="Times New Roman">which</FONT></B>和<B normal"><FONT face="Times New Roman">who</FONT></B>参数提供了一种可以用来指定一个给定用户所拥有的单个进程,一组进程或者所有进程的方法。<B normal"><FONT face="Times New Roman">who</FONT></B>要根据<B normal"><FONT face="Times New Roman">which</FONT></B>的值做出不同的解释;它会作为一个进程<FONT face="Times New Roman">ID</FONT>,进程组<FONT face="Times New Roman">ID</FONT>或者用户<FONT face="Times New Roman">ID</FONT>读取。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29220</FONT>:这是确保<B normal"><FONT face="Times New Roman">which</FONT></B>有效地进行健全性检测。我认为这里的模糊不清是不必要的。如果我们不使用</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> if ( which > 2 || which > 0 )</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>而使用如下语句</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> if ( which != PRIO_PROCESS && wich != PRIO_PGRP && which != PRIO_USER )</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>或者至少是</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> if ( which > PRIO_USER || which < PRIO_PGRP )</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>另外,在<FONT face="Times New Roman">29270</FONT>行也可以使用同样的方法。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29226</FONT>:<B normal"><FONT face="Times New Roman">niceval</FONT></B>是使用用户术语定义的——也就是说,它是在从<FONT face="Times New Roman">-20</FONT>到<FONT face="Times New Roman">19</FONT>的范围中,而不是象内核中使用的一样,在从<FONT face="Times New Roman">1</FONT>到<FONT face="Times New Roman">40</FONT>的范围中。如同变量名说明的一样,这是一个完美的值,但不是一个优先级。因此,为了实现这种转化,<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>应该跳过一些循环,同时要截断<B normal"><FONT face="Times New Roman">niceval</FONT></B>超出允许范围的值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>我承认自己被这段代码的复杂性所困扰着。使用实际上使用的<B normal"><FONT face="Times New Roman">DEF_PRIORITY</FONT></B>的值——<FONT face="Times New Roman">20</FONT>——以下的简化代码显然可以实现相同的效果:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> if ( niceval <-19 )</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> priority = 40;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> else if ( niceval > 19 )</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> priority = 1;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> else </FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> priority = 20 - niceval;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>在保持比<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>中的代码简单的同时,我的实现方法中当然也可以用于处理<B normal"><FONT face="Times New Roman">DEF_PRIORITY</FONT></B>。因此,或者我严重误解了一些内容,或者就象我提出的代码本身,它根本就不需要这么复杂。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29241</FONT>:循环遍历系统的任务列表中的所有任务,执行它可以允许修改。<B normal"><FONT face="Times New Roman">proc_sel</FONT></B>(<FONT face="Times New Roman">29190</FONT>行)说明了给定的进程是否对所提供的<B normal"><FONT face="Times New Roman">which</FONT></B>和<B normal"><FONT face="Times New Roman">who</FONT></B>值满意,可以用它来选择进程;由于<B normal"><FONT face="Times New Roman">sys_getpriority</FONT></B>也要使用这个函数,所以它也是<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>应该考虑的一个因素。</P><P 0cm 0cm 0pt">对于读取和设置单个进程优先级的普通情况(如果没有其它问题,就通过提早退出<B normal"><FONT face="Times New Roman">for_each_task</FONT></B>循环),<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>和<B normal"><FONT face="Times New Roman">sys_getpriority</FONT></B>(<FONT face="Times New Roman">29274</FONT>行开始的代码和此处有相似的内部循环)都对它有一点加速作用。<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>可能不会很频繁地被调用,但是<B normal"><FONT face="Times New Roman">sys_getpriority</FONT></B>却可能被很频繁调用,因而这样努力的是值得的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">update_process_times</H4><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>只会影响进程的<B normal"><FONT face="Times New Roman">priority</FONT></B>成员——也就是其静态优先级。回忆一下进程也是具有动态优先级的,这由<B normal"><FONT face="Times New Roman">counter</FONT></B>成员表示,这一点我们在对<B normal"><FONT face="Times New Roman">schedule</FONT></B>和<B normal"><FONT face="Times New Roman">goodness</FONT></B>的讨论中就已经清楚地看到了。我们已经可以看出在调度程序发现<B normal"><FONT face="Times New Roman">counter</FONT></B>值为<FONT face="Times New Roman">0</FONT>时,<B normal"><FONT face="Times New Roman">schedule</FONT></B>会周期性地根据其静态优先级重新计算每一个进程的动态优先级。但是我们仍然还没有看到另外一部分困扰我们的问题:<B normal"><FONT face="Times New Roman">counter</FONT></B>是在哪里被递减的?它是怎样达到<FONT face="Times New Roman">0</FONT>的?</P><P 0cm 0cm 0pt">对于<FONT face="Times New Roman">UP</FONT>,答案与<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>(<FONT face="Times New Roman">27382</FONT>行)有关。(和前面一样,我们把对于<FONT face="Times New Roman">SMP</FONT>问题的讨论延迟到第<FONT face="Times New Roman">10</FONT>章。)<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>是作为<B normal"><FONT face="Times New Roman">update_time</FONT></B>(<FONT face="Times New Roman">27412</FONT>行)的一部分被调用的,它还是第<FONT face="Times New Roman">6</FONT>章中讨论的定时器中断的一部分。作为一个结果,它被相当频繁地调用——每秒钟<FONT face="Times New Roman">100</FONT>次。(当然,这只是对人类的内力来说是相当频繁的,对于<FONT face="Times New Roman">CPU</FONT>来说这实在是很慢的。)在每一次调用的时候,它都会把当前进程的<B normal"><FONT face="Times New Roman">counter</FONT></B>值减少从上次以来经过的“滴嗒”的数目(百分之一秒——请参看第<FONT face="Times New Roman">6</FONT>章)。通常,这只是一次跳动,但是如果内核正忙于处理中断,那么内核就可能会忽略定时器的跳动。当计数器减小到<FONT face="Times New Roman">0</FONT>以下时,<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>就增加<B normal"><FONT face="Times New Roman">need_resched</FONT></B>标志,说明这个进程需要重新调度。</P><P 0cm 0cm 0pt">现在,由于进程缺省的优先级(使用内核优先级的术语,而不使用用户空间的完美值)是<FONT face="Times New Roman">20</FONT>,缺省情况下进程得到一个<FONT face="Times New Roman">21</FONT>次跳动的时间片。(的确这是<FONT face="Times New Roman">21</FONT>次跳动,而不是<FONT face="Times New Roman">20</FONT>次跳动,因为进程直到其动态优先级减少到<FONT face="Times New Roman">0</FONT>以下时才会为重新调度做出标记。)一次跳动是百分之一秒,或者是<FONT face="Times New Roman">10</FONT>微秒,因此缺省的时间片就是<FONT face="Times New Roman">210</FONT>微秒——大约是五分之一秒——在<FONT face="Times New Roman">16466</FONT>行有确切的描述。</P><P 0cm 0cm 0pt">我发现这个结果十分奇怪,因为原来以为理想的反应迅速的系统应该具有小很多的时间片——实际上我对这一点认识是如此强烈以至于开始的时候我还以为文档的说明发生了错误。但是,回顾一下,我觉得自己也不应该奇怪。毕竟,进程不会频繁地耗尽其整个时间片,因为它们经常都会因为<FONT face="Times New Roman">I/O</FONT>的原因而阻塞。在几个进程都绑定在<FONT face="Times New Roman">CPU</FONT>上时,在它们之间太频繁地跳转是没有必要的。(特别是在诸如<FONT face="Times New Roman">x86</FONT>之类的<FONT face="Times New Roman">CPU</FONT>上,这里的上下文跳转的代价是相当高的。)最后,我必须承认我从来没有注意到自己留意<FONT face="Times New Roman">Linux</FONT>逻辑单元的响应的迟缓特性,因此我觉得<FONT face="Times New Roman">210</FONT>微秒的时间片是个不错的选择——即使这在最初的时候看起来是太长了。</P><P 0cm 0cm 0pt">如果由于某些原因你需要时间片比当前最大值还长(<FONT face="Times New Roman">410</FONT>微秒,优先级上长到了<FONT face="Times New Roman">40</FONT>),你可以简单使用<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>调度策略,在你准备好以后就可以释放<FONT face="Times New Roman">CPU</FONT>(或者重新编写<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>和<B normal"><FONT face="Times New Roman">sys_nice</FONT></B>)。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">实时优先级</H2><P 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>的实时进程增加了一级优先级。实时优先级保存在<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>成员中,它是一个从<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">99</FONT>的整数。(值<FONT face="Times New Roman">0</FONT>意味着进程不是实时进程,在这种情况下其<B normal"><FONT face="Times New Roman">policy</FONT></B>成员必须是<B normal"><FONT face="Times New Roman">SCHED_OTHER</FONT></B>。)</P><P 0cm 0cm 0pt">实时任务仍然使用相同的<B normal"><FONT face="Times New Roman">counter</FONT></B>成员作为它们的非实时的计数器部分。实时任务为了某些目的甚至使用与非实时任务使用的<B normal"><FONT face="Times New Roman">priority</FONT></B>成员相同的部分,这是当时间片用完时用来补充<B normal"><FONT face="Times New Roman">counter</FONT></B>值使用的值。为了清晰起见,<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>只是用来对实时进程划分等级以对它们进行区分——否则它们的处理方式就和非实时进程相同了。</P><P 0cm 0cm 0pt">进程的<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>被设定为使用<FONT face="Times New Roman">POSIX.1b</FONT>规定的函数<B normal"><FONT face="Times New Roman">sched_setscheduler</FONT></B>和<B normal"><FONT face="Times New Roman">sched_setparam</FONT></B>(通常只有<FONT face="Times New Roman">root</FONT>才可以调用这两个函数,这一点我们在讨论权能时会看到)设置其调度策略。注意这意味着如果具有修改权限,进程的调度策略在进程生命期结束以后就可以改变。</P><P 0cm 0cm 0pt">实现这些<FONT face="Times New Roman">POSIX</FONT>函数的系统调用<B normal"><FONT face="Times New Roman">sys_sched_setscheduler</FONT></B>(<FONT face="Times New Roman">27688</FONT>行)和<B normal"><FONT face="Times New Roman">sys_sched_setparam</FONT></B>(<FONT face="Times New Roman">27694</FONT>行)都会把实际的工作交给<B normal"><FONT face="Times New Roman">setschedular</FONT></B>(<FONT face="Times New Roman">27618</FONT>行)处理,这个函数我们现在就介绍。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">setscheduler</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27618</FONT>:这个函数的三个参数是目标进程<B normal"><FONT face="Times New Roman">pid</FONT></B>(<FONT face="Times New Roman">0</FONT>意味着当前进程),新的调度策略<B normal"><FONT face="Times New Roman">policy</FONT></B>,和包含附加信息的一个结构<B normal"><FONT face="Times New Roman">param</FONT></B>——它记录了<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>的新值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27630</FONT>:在一些健全性检测之后,<B normal"><FONT face="Times New Roman">setscheduler</FONT></B>从用户空间中得到提供的<B normal"><FONT face="Times New Roman">struct sched_param</FONT></B>结构的备份。在<FONT face="Times New Roman">16204</FONT>行定义的<B normal"><FONT face="Times New Roman">struct sched_param</FONT></B>结构只有一个成员<B normal"><FONT face="Times New Roman">sched_priority</FONT></B>,它就是调用者为目标进程设计的<B normal"><I normal"><FONT face="Times New Roman">rt_priority</FONT></I></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27639</FONT>:使用<B normal"><FONT face="Times New Roman">find_process_by_pid</FONT></B>(<FONT face="Times New Roman">27608</FONT>行)找到目标进程,如果<B normal"><FONT face="Times New Roman">pid</FONT></B>是<FONT face="Times New Roman">0</FONT>,这个函数就返回一个指向当前任务的指针;如果存在指向具有给定<FONT face="Times New Roman">PID</FONT>进程,就返回指向该进程的指针;或者如果不存在具有这个<FONT face="Times New Roman">PID</FONT>的进程,就返回<B normal"><FONT face="Times New Roman">NULL</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27645</FONT>:如果<B normal"><FONT face="Times New Roman">policy</FONT></B>参数为负时,就保留当前的调度策略。否则,如果这是个有效值,那么现在就可以将其接收。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27657</FONT>:确保优先级没有越界。这是通过使用一点小技巧来加强的。该行只是第一步,它被用来确保所提供的值没有大得超出了范围。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27659</FONT>:现在已经确知新的实时优先级位于<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">99</FONT>的范围之内。如果<B normal"><FONT face="Times New Roman">policy</FONT></B>是<B normal"><FONT face="Times New Roman">SCHED_OTHER</FONT></B>,但是新的实时优先级不是<FONT face="Times New Roman">0</FONT>,那么这个测试就失败了。如果<B normal"><FONT face="Times New Roman">policy</FONT></B>指明了一个实时调度程序但是新的实时优先级是<FONT face="Times New Roman">0</FONT>(如果这里它不是<FONT face="Times New Roman">0</FONT>,就应该是从<FONT face="Times New Roman">1</FONT>到<FONT face="Times New Roman">99</FONT>),测试也会失败。否则,测试就能成功。这虽然并不是很易读,但它确实是正确的、最小的,(我想)速度也很快。我不确定这里我们是否对速度有所苛求,但是——到底一个进程需要多长时间需要设置它的调度程序?下面的代码就应该具有更好的可读性,而且当然也不会太慢:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt; mso-outline-level: 1"><FONT face="Times New Roman"> P492 1</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27663</FONT>:不是每一个进程都可以设置自己的调度策略和其它进程的调度策略。如果所有进程都可以设置自己的调度策略,那么任何进程都可以简单地设置自己的调度策略为<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>并进入一个无限循环来抢占<FONT face="Times New Roman">CPU</FONT>,这样必然会锁定系统。显然,是不能够允许这种做法的。因此,只有进程拥有这样处理的权能时,<B normal"><FONT face="Times New Roman">setscheduer</FONT></B>才会允许进程设置自己的调度策略。权能在下一节中将比较详细地介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27666</FONT>:在相同的行中,我们不希望别人可以修改其它用户进程的调度策略;普通情况下,只允许你修改你自己所有的进程的调度策略。因此,<B normal"><FONT face="Times New Roman">setscheduer</FONT></B>要确保或者用户是设置自己所有的进程的处理程序或者具有修改其它进程的调度策略的权能。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27672</FONT>:这里才是<B normal"><FONT face="Times New Roman">setscheduler</FONT></B>实际工作的地方,它在目标进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构中设置<B normal"><FONT face="Times New Roman">policy</FONT></B>和<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>。如果该进程在运行队列中(这通过检测其<B normal"><FONT face="Times New Roman">next_run</FONT></B>成员非空来测试),就将它移到运行队列的顶部——这比较容易令人感到迷惑;可能这有助于<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>进程实现对<FONT face="Times New Roman">CPU</FONT>的抢占。进程为重新调度做出标记,<B normal"><FONT face="Times New Roman">setscheduer</FONT></B>清空并退出。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">遵守限制</H2><P 0cm 0cm 0pt">内核经常需要决定是否允许进程执行某个操作。进程可能被简单的禁止执行某些操作,但却被允许在受限的环境中执行一些别的操作;这些操作基本上可以由权能表示,并且<FONT face="Times New Roman">/</FONT>或者可以从用户<FONT face="Times New Roman">ID</FONT>和组<FONT face="Times New Roman">ID</FONT>中推导出来。在其它期间,允许进程处理一些操作,但只是在受限的环境中——例如,它对<FONT face="Times New Roman">CPU</FONT>的使用必须受到限制。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>权能</FONT></H3>在前面一节中,你已经看到了一个检测权能的例子——实际上是有两次相同的权能。这是<B normal">CAP_SYS_NICE</B>权能(14104行),它决定是否应该允许进程设置优先级(完美级别)