荔园在线

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

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


发信人: Ewi.bbs@siyu.dhs.org (Ewi), 信区: Linux
标  题: Linux内核注释—— 第5章 系统调用
发信站: 思雨小语 (Sun May 25 18:46:36 2003)
转信站: SZU!news.tiaozhan.com!Siyu


大部分介绍Unix内核的书籍都没有仔细说明系统调用,我认为这是一个失误。实际
上,我们实际需要的系统调用现在已经十分完美。因此,从某种意义上来说,研究
系统调用的实现是无意义的——如果你想为Linux内核的改进贡献自己的力量,还
有其它许多方面更值得投入精力。
然而,对于我们来说,仔细研究少量系统调用是十分值得的。这样就有机会初步了
解一些概念,这些概念将随本书发展而进行详细介绍,例如进程处理和内存。这使
得你可以趁机详细了解一下Linux内核编程的特点。这包括一些和你过去在学校里
(或工作中)所学的内容不同的方法。和其它编程任务相比,Linux内核编程的一
个显著特点是它不断同三个成见进行斗争——这三个成见就是速度、正确和清晰—
—我们不可能同时获取这三个方面…至少并不总是能够。
什么是系统调用
系统调用发生在用户进程(比如emacs)通过调用特殊函数(例如open)以请求内
核提供服务的时候。在这里,用户进程被暂时挂起。内核检验用户请求,尝试执行
,并把结果反馈给用户进程,接着用户进程重新启动,随后我们就将详细讨论这种
机制。
系统调用负责保护对内核所管理的资源的访问,系统调用中的几个大类主要有:处
理I/O请求(open,close,read,write,poll等等),进程(fork,execve,
kill,等等),时间(time,settimeofday等等)以及内存(mmap,brk,等等)
的系统调用。几乎所有的系统调用都可以归入这几类中。
然而,从根本上来说,系统调用可能和它表面上有所不同。首先,在Linux中,C库
中对于一些系统调用的实现是建立在其它系统调用的基础之上的。例如,waitpid
是通过简单调用wait4实现的,但是它们两个都是作为独立的系统调用说明的。其
它的传统系统调用,如sigmask和ftime是由C库而不是由Linux内核本身实现的;即
使不是全部,至少大部分是如此。
当然,从技巧的一面来看这是无害的——从应用程序的观点来看,系统调用就和其
它的函数调用一样。只要结果符合预计的情况,应用程序就不能确定是否真正使用
到了内核。(这种处理方式还有一个潜在的优点:用户可以直接触发的内核代码越
少,出现安全漏洞的机会也就越少。)但是,由于使用这种技巧所引起的困扰将会
使我们的讨论更为困难。实际上,系统调用这一术语通常被演讲者用来说明在第一
个Unix版本中的任何对系统的调用。但是在本章中我们只对“真正”的系统调用感
兴趣——真正的系统调用至少包括用户进程对部分内核代码的调用。
系统调用必须返回int的值,并且也只能返回int的值。为了方便起见,返回值如果
为零或者为正,就说明调用成功;为负则说明发生了错误。就像老练的C程序员所
知道的一样,当标准C库中的函数发生错误时会通过设置全局整型变量errno指明发
生错误的属性,系统调用的原理和它相同。然而,仅仅研究内核源程序代码并不能
够获得这种系统调用方式的全部意义。如果发生了错误,系统调用简单返回自己所
期望的负数错误号,其余部分则由标准C库实现。(正常情况下,用户代码并不直
接调用内核系统函数,而是要通过标准C库中专门负责翻译的一个小层次(thin
layer)实现。)我们随便举一个例子,27825行(sys_nanosleep的一部分)返回
-EINVAL指明所提供的值越界了。标准C库中实际处理sys_nanosleep的代码会注意
到返回的负值,从而设置errno和EINVAL,并且自己返回-1给原始的调用者。
在最近的内核版本中,系统调用返回负值偶尔也不一定表示错误了。在目前的几个
系统调用中(例如lseek),即使结果正确也会返回一个很大的负值。最近,错误
返回值是在-1到-4095范围之内。现在,标准C库实现能够以更加成熟和高级的方式
解释系统调用的返回值;当返回值为负时,内核本身就不用再做任何特殊的处理了

中断、内核空间和用户空间
我们将在第6章中介绍中断和在第8章中介绍内存时再次明确这些概念。但是在本章
中,我们只需要粗略地了解一些术语。
第一个术语是中断(interrupt),它来源于两个方面:硬件中断,例如磁盘指明
其中存放一些数据(这与本章无关);和软件中断,一种等价的软件机制。在x86
系列CPU中,软件中断是用户进程通知内核需要触发系统调用的基本方法(出于这
种目的使用的中断号是0x80,对于Intel芯片的研究者来说更为熟悉的是INT 80)
。内核通过system_call(171行)函数响应中断,这一点我们马上就会介绍。
另外两个术语是内核空间(kernel space)和用户空间(user space),它们分别
对应内核保留的内存和用户进程保留的内存。当然,多用户进程也经常同时运行,
而且各个进程之间通常不会共享它们的内存,但是,任何一个用户进程使用的内存
都称为用户空间。内核在某一个时刻通常只和一个用户进程交互,因此实际上不会
引起任何混乱。
由于这些内存空间是相互独立的,用户进程根本不能直接访问内核空间,内核也只
能通过put_user(13278行)和get_user(13254行)宏和类似的宏才可以访问用户
空间。因为系统调用是进程和进程所运行的操作系统之间的接口,所以系统调用需
要频繁地和用户空间交互,因此这些宏也就会不时的在系统调用中出现。在通过数
值传递参数的情况下并不需要它们,但是当用户把指针——内核通过这个指针进行
读写——传递给系统调用时,就需要这些宏了。
如何激活系统调用
系统调用的的激活有两种方法:system_call函数和lcall7调用门(call gate)(
请参看135行)。(你可能听说过还有一种机制,syscall函数,是通过调用
lcall7实现的——至少在x86平台上是如此——因此,它并不是一个特有的方法。
)本节将细致地讨论一下这两种机制。
在阅读的过程中请注意系统调用本身并不关心它们是由system_call还是由lcall7
激活的。这种把系统调用和其实现方式区别开来的方法是十分精巧的。这样,如果
出于某种原因我们不得不增加一种激活系统调用的方法,我们也不必修改系统调用
本身来支持这种方法。
在你浏览这些汇编代码之前要注意这些机器指令中操作数的顺序和普通Intel的次
序相反。虽然还有一些其它的语法区别,但是操作数反序是最令人迷惑的。如果你
还记得Intel的语法:
mov eax, 0
(本句代码的意思是把常数0传送到寄存器EAX中)在这里应该写作:
mov1 $0, %eax
这样你就能够正确通过。(内核使用的语法是AT&T的汇编语法。在GNU汇编文档中
有更多资料。)
system_call
system_call(171行)是所有系统调用的入口点(这是对于内部代码来说的;
lcall7用来支持iBCS2,这一点我们很快就会讨论)。正如前面标题注释中说明的
一样,目的是为普通情况简单地实现直接的流程,不采用跳转,因此函数的各个部
分都是离散的——整体的流量控制已经因为要避免普通情况下的多分支而变得非常
复杂。(分支的避免是十分值得的,因为它们引起的代价非常昂贵。它们可以清空
CPU管道,使现存CPU的并行加速机制失效。)
图5.1  system_call的流程控制

图5.1显示了作为system_call的一部分出现的分支目标标签以及它们之间的流程控
制方向,该图可以在你阅读本部分讨论内容时提供很大的帮助。图中system_call
和restore_all两个标签比其它标签都要大,因为这两处是该函数正常的出口点和
入口点;然而,还有另外两个入口点,这一点在本章的后续内容中很快就可以看到

system_call是由标准C库激活的,该标准C库会把自己希望传递的参数装载到CPU寄
存器中,并触发0x80软件中断。(system_call在这里是一个中断处理程序。)内
核记录了软件中断和6828行的system_call函数的联系(SYSCALL_VECTOR是在1713
行宏定义为0x80的)。
system_call
172:  system_call的第一个参数是所希望激活的系统调用的数目;它存储在EAX
寄存器中。system_call还允许有多达四个的参数和系统调用一起传送。在一些极
其罕见的情况下使用四个参数的限制是负担繁重的,通常可以建立一个指向结构的
指针参数来巧妙地完成同样功能,指针指向的结构中可以包含你所需要的一切信息

    随后可能需要EAX值的一个额外拷贝,因此通过将其压栈而保存起来;这个值就是
218行的ORIG_EAX(%esp)表达式的值。
173:   SAVE_ALL宏是在85行定义的;它把所有寄存器的值压入CPU的堆栈。随后,
就在system_call返回之前,使用RESTALL_ALL(100行)把栈中的值弹出。在这中
间,system_call可以根据需要自由使用寄存器的值。更重要的是,任何它所调用
的C函数都可以从栈中查找到所希望的参数,因为SAVE_ALL已经把所有寄存器的值
都压入栈中了。
    结果栈的结构从26行开始描述。象0(%esp)和4(%esp)一样的表达式指明了堆
栈指针(ESP寄存器)的一种替换形式——分别表示ESP上的0字节,ESP上的4字节
,等等。特别要注意的是在前面一行中压入堆栈的EAX的拷贝已经变成本标题注释
作为orig_eax所描述的内容;它们是由SAVE_ALL压入寄存器之上的堆栈的(
orig_eax之上的寄存器在这里早已就绪了)。
    还需注意:这可能有点令人迷惑——由于我们调用orig_eax时EAX的拷贝已经压入
了堆栈,它是否有可能在其它寄存器下面而不是在其它寄存器上面呢?答案既是肯
定的,也是否定的。x86的堆栈指针寄存器ESP在有数据压入堆栈时会减少——堆栈
会向内存低地址发展。因此,orig_eax逻辑上是在其它值的下面,但是物理上却是
在其它值的上面。
    从51行开始的一系列宏有助于使这些替换更容易理解。例如,EAX(%esp)就和
18(%esp)相同——然而前一种方法通过表达式引用存储在堆栈中的EAX寄存器副
本的决定可以使整个过程更加简单。
174:   从EBX寄存器中取得指向当前任务的指针。完成这个工作的宏GET_CURRENT(
131行)对于在大部分代码中使用的C函数get_current(10277行)来说是一个无限
循环。
    此后,当看到类似于foo(%ebx)或者foo(%esp)的表达式时,这意味着这些的
代码正在引用代表当前进程的结构的字段——16325行的struct task_struct——
这在第7章中将对它进行更详细的介绍。(更确切的描述是, %ebx的置换在struct
 task_struct中,%esp的置换在与struct task_struct相关联的struct pt_regs结
构中。但是这些细节在这里都并不重要。)
175:   检查(EAX中的)系统调用的数目是否超过系统调用的最大数量。(此处
EAX为一个无符号数,因此不可能为负值。)如果的确超过了,就向前跳转到
badsys(223行)。
177:   检测系统调用是否正被跟踪。如strace之类的程序为有兴趣的人提供了系统
调用的跟踪工具,或者额外的调试信息:如果能够监测到正在执行的系统调用,那
么你就可以了解到当前程序正在处理的内容。如果系统调用正被跟踪,控制流程就
向前跳转到tracesys(215行)。
179:   调用系统函数。此处有很多工作需要处理。首先,SYSMOL_NAME宏不处理任
何工作,只是简单的为参数文本所替换,因此可以将其忽略。sys_call_table是在
当前文件(arch/i386/kernel/entry.S)的末尾从373行开始定义的。这是一张由
指向实现各种系统调用的内核函数的函数指针组成的表。
    本行中第二对圆括号中包含了三个使用逗号分割开的参数(第一个参数为空);
这里就是实现数组索引的地方。当然,这个数组是以sys_call_table作为索引的,
这称为偏移(displacement)。这三个参数是数组的基地址、索引(EAX,系统调
用的数目)和大小,或者每个数组元素中的字节数——在这里就是4。由于数组基
地址为空,就将其当作0——但是它要和偏移地址,sys_call_table,相加,简单
的说就是sys_call_table被当作数组的基地址。本行基本上等同于如下的C表达式

    /* Call a function in an array of functions. */
    (sys_call_table[eax])();
    然而, C当然还要处理许多繁重的工作,例如为你记录数组元素的大小。不要忘
记,系统调用的参数早已经存储在堆栈中了,这主要由调用者提供给system_call
并使用SAVE_ALL把它们压栈。
180:   系统调用已经返回。它在EAX寄存器中的返回值(这个值同时也是
system_call的返回值)被存储起来。返回值被存储在堆栈中的EAX内,以使得
RESTORE_ALL可以迅速地恢复实际的EAX寄存器以及其它寄存器的值。
182:   接下来的代码仍然是system_call的一部分,它是一个也可以命名为
ret_from_sys_call和ret_from_intr的独立入口点。它们偶尔会被C直接调用,也
可以从system_call的其它部分跳转过来。
185:   接下来的几行检测“下半部分(bottom half)”是否激活;如果激活了,
就跳转到handle_bottom_half标号(242行)并立即开始处理。下半部分是中断进
程的一部分,将在下一章中讨论。
189:   检查该进程是否为再次调度做了标记(记住表达式$0就是常量0的系统简单
表示)。如果的确如此,就跳转到reschedule标号(247行)。
191:   检测是否还有挂起的信号量,如果有的话,下一行就向前跳转到
signal_return(197行)。
193:   restore_all标号是system_call的退出点。其主体就是简单的RESTORE_ALL
宏(100行),该宏将恢复早先由SAVE_ALL存储的参数并返回给system_call的调用
者。
197:   当system_call从系统调用返回前,如果它检测到需要将信号量传送给当前
的进程时,才会执行到signal_return。它通过使中断再次可用开始执行,有关内
容将在第6章中介绍。
199:   如果返回虚拟8086模式(这不是本书的主题),就向前跳转到
v86_signal_return(207行)。
202:   system_call要调用C函数do_signal(3364行,在第6章中讨论)来释放信号
量。do_signal需要两个参数,这两个参数都是通过寄存器传递的;第一个是EAX寄
存器,另一个是EDX寄存器。system_call(在200行)早已把第一个参数的值赋给
了EAX;现在,就把EDX寄存器和寄存器本身进行XOR操作,从而将其清0,这样
do_signal就认为这是一个空指针。
203:   调用do_signal传递信号量,并且跳回到restore_all(193行)结束。
207:   由于虚拟8086模式不是本书的主题,我们将忽略大部分v86_signal_return
。然而,它和signal_return的情况非常类似。
215:   如果当前进程的系统调用正由其祖先跟踪,就像strace程序中那样,那么就
可以执行到tracesys标号。这一部分的基本思想如同179行一样是通过
syscall_table调用系统函数,但是这里把该调用和对syscall_trace函数的调用捆
绑在一起。后面的这个函数在本书中并没有涉及到,它能够中止当前进程并通知其
祖先注意当前进程将要激活一个系统调用。
    EAX操作和这些代码的交错使用最初可能容易令人产生困惑。system_call把存储
在堆栈中的EAX拷贝赋给-ENOSYS,调用syscall_trace,在172行再从所做的备份中
恢复EAX的值,调用实际的系统调用,把系统调用的返回值置入堆栈中EAX的位置,
再次调用syscall_trace。
    这种方式背后的原因是syscall_trace(或者更准确的说是它所要使用到的跟踪程
序)需要知道在它是在实际系统调用之前还是之后被调用的。-ENOSYS的值能够用
来指示它是在实际系统调用执行之前被调用的,因为实际中所有实现的系统调用的
执行都不会返回-ENOSYS。因此,EAX在堆栈中的备份在第一次调用syscall_trace
之前是-ENOSYS,但是在第二次调用syscall_trace之前就不再是了(除非是调用
sys_ni_syscall的时候,在这种情况下,我们并不关心是怎样跟踪的)。218行和
219行中EAX的作用只是找出要调用的系统调用,这和无须跟踪的情况是一致的。
222:   被跟踪的系统调用已经返回;流程控制跳转回ret_from_sys_call(184行)
并以与普通的无须跟踪的情况相同的方式结束。
223:   当系统调用的数目越界时,就可以执行到badsys标号。在这种情况下,
system_call必须返回-ENOSYS(ENOSYS在82行将它赋值为38)。正如前面提到的一
样,调用者会识别出这是一个错误,因为返回值在-1到-4,095之间。
228:   在诸如除零错误(请参看279行)之类的CPU异常中断情况下将执行到
ret_from_exception标号;但是system_call内部的所有代码都不会执行到这个标
号。如果有下半部分是激活的,现在就是它在起作用了。
233:   处理完下半部分之后或者从上面的情况简单的执行下来(虽然没有下半部分
是激活的,但是同样也触发了CPU异常),就执行到了ret_from_intr标号。这是一
个全局符号变量,因此可能在内核的其它部分也会有对它的调用。
237:   被保存的CPU的EFLAGS和CS寄存器在此已经被并入EAX,因而高24位的值(其
中恰好包含了一位在70行定义的非常有用的VM_MASK)来源于EFLAGS,其它低8位的
值来源于CS。该行隐式的同时对这两部分进行测试以判断进程到底返回虚拟8086模
式(这是VM_MASK的部分)还是用户模式(这是3的部分——用户模式的优先等级是
3)。下面是近似的等价C代码:
    /* Mix eflags and cs in eax. */
    eax = eflags & ~0xff;
    eax |= cs & ~0xff
    /* Simultaneously test lower 2 bits
       * and VM_MASK bit. */
    if  (eax & (VM_MASK | 3))
       goto ret_with_reschedule;
    goto restore_all;
238:   如果这些条件中有一个能得到满足,流程控制就跳转到
ret_with_reschedule(188行)标号来测试在system_call返回之前进程是否需要
再次调度。否则,调用者就是一个内核任务,因此system_call通过跳转到
restore_all (193行)来跳过重新调度的内容。
242:   无论何时system_call使用一个下半部分服务时都可以执行到
handle_bottom_half标号。它简单的调用第6章中介绍的C函数bottom_half(
29126行),然后跳回到ret_from_intrr(233行)。
248:   system_call的最后一个部分在reschedule标号之下。当产生系统调用的进
程已经被标记为需要进行重新调度时,就可以执行到这个标号;典型地,这是因为
进程的时间片已经用完了——也就是说,进程到目前为止已经尽可能的拥有CPU了
,应该给其它进程一个机会来运行了。因此,在必要的情况下就可以调用C函数
schedule(26686行)交出CPU,同时流程控制转回249行。CPU调度是第7章中讨论
的一个主题。
lcall7
Linux支持Intel二进制兼容规范标准的版本2(iBCS2)。(iBCS2中的小写字母i显
然是有意的,但是该标准却没有对此进行解释;这样看来似乎和现实的Intel系列
的CPU例如i386,i486等等是一致的。)iBCS2的规范中规定了所有基于x86的Unix
系统的应用程序的标准内核接口,这些系统不仅包括Linux,而且还包括其它自由
的x86 Unix(例如FreeBSD),也还包括Solaris/x86,SCO Unix等等。这些标准接
口使得为其它Unix系统开发的二进制商业软件在Linux系统中能够直接运行,反之
亦然(而且,近期新开发软件向其它Unix移植的情况越来越多)。例如,Corel公
司的WordPerfect的SCO Unix的二进制代码在还没有Linux的本地版本的
WordPerfect之前就可以使用iBCS2在Linux上良好地运行。
iBCS2标准有很多组成部分,但是我们现在关心的是这些系统调用如何协调一致来
适应这些迥然不同的Unix系统。这是通过lcall7调用门实现的。它是一个相当简单
的汇编函数(尤其是和system_call相比而言更是如此),仅仅定位并全权委托一
个C函数来处理细节。(调用门是x86 CPU的一种特性,通过这种特性用户任务可以
在安全受控的模式下调用内核代码。)这种调用门在6802行进行设定。
lcall7
136: 前面的几行将通过调整处理器堆栈以使堆栈的内容和system_call预期的相
同——system_call中的一些代码将会完成清理工作,这样所有的内容都可以连续
存放了。
145:   基于同样的思想,lcall7把指向当前任务的指针置入EBX寄存器,这一点和
system_call的情况是相同的。但是,它的执行方式却与system_call不同,这就比
较奇怪了。这三行可以等价地按如下形式书写:
    push1 %esp
    GET_CURRENT(%ebx)
    这种实现的执行速度并不比原有的更快;在将宏展开以后,实际上这还是同样的
三条指令以不同的次序组合在一起而已。这样做的优点是可以和文件中的其它代码
更为一致,而且代码也许会更清晰一些。
148:   取得指向当前任务exec_domain域的指针,使用这个域以获取指向其lcall7
处理程序的指针,接着调用这个处理程序。
    本书中并没有对执行域(execution domains)进行详细说明——但是简单说来,
内核使用执行域实现了部分iBCS2标准。在15977行你可以找到struct
exec_domain结构。default_exec_domain(22807行)是缺省的执行域,它拥有一
个缺省的lcall7处理程序。它就是no_lcall7(22820行)。其基本的执行方式类似
于SVR4风格的Unix,如果调用进程没有成功,就传送一个分段违例信号量(
segmentation violation signal)给调用的进程,。
152:   跳转到ret_from_sys_call标号(184行——注意这是在system_call内部的
)清除并返回,就像是正常的系统调用一样。
系统调用样例
现在你已经知道了系统调用是如何激活的,接下来我们将通过几个系统调用例子的
剖析来了解一下它们的工作方式。注意系统调用foo几乎都是使用名为sys_foo的内
核函数实现的,但是在某些情况下该函数也会使用一个名为do_foo的辅助函数。
sys_ni_syscall
29185:sys_ni_syscall的确是最简单的系统调用;它只是简单的返回ENOSYS错误
。最初的时候这可能显得没有什么作用,但是它的确是有用的。实际上,
sys_ni_syscall在sys_call_table中占据了很多位置——而且其原因并不只有一个
。开始的时候,sys_ni_syscall在位置0(374行),因为如果漏洞百出的代码错误
地调用了system_call——例如,没有初始化作为参数传递给system_call的变量—
—在这种偶然的变量定义中,0是最可能的值。如果我们能够避免这种情况,那么
在错误发生时就不用采取象杀掉进程一样的剧烈措施。(当然,只要允许有用工作
的进行,就不可能防止所有的错误。)这种使用表的元素0作为抵御错误的手段在
内核中被作为良好的经验而广泛使用。
    而且,你还会发现sys_ni_syscall在表中明显出现的地方就多达十几处。这些条
目代表了那些已经从内核中移出的系统调用——例如在418行,就代替了已经废弃
了的prof系统调用。我们不能简单地把另外的实际系统调用放在这里,因为老的二
进制代码可能还会使用到这些已经废弃了的系统调用号。如果一个程序试图调用这
些老的系统调用,但是结果却与预期的完全不同,例如打开了一个文件,这会比较
令人感到奇怪的。
    最后,sys_ni_syscall将占据表尾部所有未用的空间;这一点是在从572行到574
行的代码实现的,它根据需要重复使用这些项来填充表。由于sys_ni_syscall只是
简单返回ENOSYS错误号,对它的调用和跳转到system_call中的badsys标号作用是
相同的——也就是说,使用指向这些表项的系统调用号和在表外对整个表进行全部
索引具有相同的作用。因此,我们不用改变NR_syscalls就可以在表中增加(或者
删除)系统调用,但是其效果与我们真的对NR_syscalls进行了修改一样(不管怎
样,这都是由NR_syscalls所建立的限制条件所决定的)。
    到现在也许你已经猜到了,sys_ni_syscall中的“ni”并不是指Monty Python的
“说 ‘Ni’ 的骑士”;而是指“not implemented(没有实现)”这一相较而言
并不太诙谐的短语。
    对于这个简单的函数我们需要研究的另外一个问题是asmlinkage标签。这是为一
些gcc功能定义的一个宏,它告诉编译器该函数不希望从寄存器中(这是一种普通
的优化 )取得任何参数,而希望仅仅从CPU堆栈中取得参数。回忆一下我们前面提
到过system_call使用第一个参数作为系统调用的数目,同时还允许另外四个参数
和系统调用一起传递。system_call通过把其它参数(这些参数是通过寄存器传递
过来的)滞留在堆栈中的方法简单的实现了这种技巧。所有的系统调用都使用
asmlinkage标签作了标记,因此它们都要查找堆栈以获得参数。当然,在
sys_ni_syscall的情况下这并没有任何区别,因为sys_ni_syscall并不需要任何参
数。但是对于其它大部分系统调用来说这就是个问题了。并且,由于在其它很多函
数前面都有asmlinkage标签,我想你也应该对它有些了解。
sys_time
31394:sys_time是包含几个重要概念的简单系统调用。它实现了系统调用time,
返回值是从某个特定的时间点(1970年1月1日午夜UTC)以来经过的秒数。这个数
字被作为全局变量xtime(请参看26095行;它被声明为volatile型的变量,因为它
可以通过中断加以修改,这一点我们在第6章中就会看到)的一部分,通过
CURRENT_TIME宏(请参看16598行)可以访问它。
31400:该函数非常直接的实现了它的简单定义。当前时间首先被存储在局部变量
i中。
31402:如果所提供的指针tloc是非空的,返回值也将被拷贝到指针指向的位置。
该函数的一个微妙之处就在于此;它把i拷贝到用户空间中而不是使用
CURRENT_TIME宏来重新对其进行计算,这基于两个原因:
?   CURRENT_TIME宏的定义以后可能会改变,新的实现方法可能会由于某种原因而速
度比较慢,但是对于i的访问至少应该和CURRENT_TIME宏展开的速度同样快。
?   使用这种方式处理,确保结果的一致性:如果代码刚好执行到31400行和31402行
之间时时间发生了改变,sys_time可能把一个值拷贝到*tloc中,但是在结束之后
却返回另一个值。
    另外还有一个小的方面需要注意,此处的代码不使用&&来编写而是使用两个if,
这可能有一点令人奇怪。内核中采用这些看起来非常特殊的代码的一般原因都是由
于速度的要求,但是gcc为&&版本和两个if版本的代码生成的代码是等同的,因此
这里的原因就不可能是速度的要求——除非这些代码是在早期gcc版本下开发的,
这样才有些意义。
31403:如果sys_time不能访问所提供的位置(一般都是因为tloc无效),它就把
-EFAULT的值赋给i,从而在31405行返回错误代码。
31405:为调用者返回的i或者是当前时间,或者是-EFAULT。
sys_reboot
29298:内核中其他地方可能都没有sys_reboot的实现方法这样先进。其原因是可
以理解为:根据调用的名字我们就可以知道,reboot系统调用可以用来重新启动机
器。根据所提供的参数,它还能够挂起机器,关闭电源,允许或者禁止使用
Ctrl+Alt_Del组合键来重启机器。如果你要使用这个函数编写代码,需要特别注意
它上面的注释标题的警告:首先同步磁盘,否则磁盘缓冲区中的数据可能会丢失。

    由于它可能为系统引发的潜在后果,sys_reboot需要几个特殊参数,这一点马上
就会讨论。
29305:如果调用者不具有CAP_SYS_BOOT(14096行)权能(capability),系统就
会返回EPERM错误。权能在第7章中会详细讨论。现在,简单的说就是:权能是检测
用户是否具有特定权限的方法。
29309:在这里,这种偏执的思想充分发挥了作用。syst_reboot根据从16002到
16005行定义的特殊数字检测参数magic1和magic2。这种思想是如果sys_reboot在
某种程度上是被偶然调用的,那么就不太可能再从由magic1和magic2组成的小集合
中同时提取值。注意这并不意味着这是一个防止粗心的安全措施。
    顺便说一下,这些特殊数字并不是随机选取的。第一个参数的关系是十分明显的
,它是“感受死亡(feel dead)”的双关语。后面的三个参数要用十六进制才能
了解它们全部的意思:它们分别是0x28121969,0x5121996,0x16041998。这似乎
代表Linus的妻子(或者就是Linus自己)和他两个女儿的生日。由此推论,当
Linus和他的妻子养育了更多儿女的时候,重启动需要的特殊参数可能在某种程度
上会增加。不过我想在他们用尽32位可能空间之前,他的妻子就会制止他的行为了

29315:请求内核锁,这样能保证这段代码在某一时间只能由一个处理器执行。使
用lock_kernel/unlock_kernel函数对所保护起来的任何其它代码对其它CPU都同样
是不可访问的。在单处理器的机器中,这只是一个no-op(不处理任何事情);而
详细讨论它在多处理器上的作用则是第10章的内容。
29317:在LINUX_REBOOT_CMD_RESTART的情况中,sys_reboot调用一系列基于
reboot_notifier_list的函数来通知它们系统正在重新启动。正常情况下,这些函
数都是操作系统关闭时需要清除的模块的一部分。这个列表函数似乎并不在内核中
的其它地方使用——至少在标准内核发行版本中是这样,也许此外的其它模块可能
使用这个列表。不管怎样,这个列表的存在可以方便其他人使用。
    LINUX_REBOOT_CMD_RESTART和其它cmd识别出的值从16023行开始通过#define进行
宏定义。这些值并没有潜在的意义,选用它们的简单原因是它们一般不会发生意外
并且相互之间各不相同。(有趣的是,LINUX_REBOOT_CMD_OFF是零,这是在意外情
况下最不可能出现的一个值。但是,由于LINUX_REBOOT_CMD_OFF简单的禁止用户使
用Ctrl+Alt+Del重新启动机器,它就是一种“安全”的意外了。)
29321:打印警告信息以后,sys_reboot调用machine_restart(2185行)重启机器
。正如你从2298行中所看到的一样,machine_restart函数从来不会返回。但是不
管怎样,对于machine_restart的调用后面都跟着一个break语句。
    这仅仅是经典的良好的编程风格吗?的确如此,但是却又不仅仅如此。文件
kernel/sys.c的代码是属于体系结构无关部分的。但是machine_restart,它显然
是体系结构所特有的,属于代码的体系结构特有的部分(
arch/i386/kernel/process.c)。因而对于不同的移植版本也有所不同。我们并不
清楚以后内核的每个移植版本的实现都不会返回——例如,它可能调度底层硬件重
启但是本身要仍然持续运行几分钟,这就需要首先从函数中返回。或者更为确切的
说法是,由于某些特定的原因,系统可能并不总是能够重启;或许某些软件所控制
的硬件根本就不能重启。在这种平台上,machine_restart就应该可以返回,因此
体系结构无关的代码应该对这种可能性有所准备。
    针对这个问题,正式的发行版本中都至少包含一个退出端口,使
machine_restart函数可以从这个端口返回:m68k端口。不同的基于m68k的机器支
持的代码也各不相同,由于本书主要是针对x86的,我不希望花费过多的时间来解
析所有的细节。但是这的确是可能的。(在其它情况下,machine_restart简单进
入一个无限循环——既不重新启动机器,也不返回。但是这里我们担心的是需要返
回的情况。)
    因此,我们毕竟是需要break的。前面看起来只是简单的习惯甚至是偏执的思想在
这里为了内核的移植性已经变成必须的了。
29324:接下来的两种情况分别允许和禁止臭名卓著的Ctrl+Alt+Del组合键(这三
个组合键也被称为“Vulcan神经收缩(Vulcan nerve pinch)”,“黑客之手(
hacker’s claw)”,“三指之礼(three-fingered salute)”,我最喜欢后面
这个)。这些只是简单的设置全局C_A_D标志(在29160行定义,在29378行检测)

29332:这种情况和LINUX_REBOOT_CMD_RESTART类似,但只是暂停系统而不是将其
重新启动。两者之间的一个区别是它调用machine_halt(2304行)——这是x86上
的一条no-op指令,但是在其它平台上却要完成关闭系统的实际工作——而不是调
用machine_restart。并且它会把machine_halt不能使之暂停的机器转入低功耗模
式运行。它使用do_exit(23267行)杀死内核本身。
29340:到现在为止,这已经是一种比较熟悉的模式了。这里,sys_reboot关闭机
器电源,除了为可以使用软件自行关闭电源的系统调用machine_power_off(2307
行)之外,其它的应该和暂停机器情况完全相同。
29348:LINUX_REBOOT_CMD_RESTART2的情况是已建立主题的一个变种。它接收命令
,将其作为ASCII字符串传递,该字符串说明了机器应该如何关闭。字符串不会由
sys_reboot本身来解释,而是使用machine_restart函数来解释;因而这种模式的
意义,如果有的话,就是这些代码是平台相关的。(我使用“如果有”的原因是启
动机器——特别是在x86中——一般只有一种方法,因此其它的信息都可以被
machine_restart忽略。)
29365:调用者传递了一个无法识别的命令。sys_reboot不作任何处理,仅仅返回
一个错误。因此,即使由magic1和magic2传递给sys_reboot正确的magic数值,它
也无须处理任何内容。
29369:一个可识别的命令被传递给sys_reboot。如果流程执行到这里,它可能就
是两个设置C_A_D的命令之一,因为其它情况通常都是停止或者重新启动机器。在
任何情况下,sys_reboot都简单把内核解锁并返回0以表示成功。
sys_sysinfo
24142:一个只能返回一个整型值的系统调用。如果需要返回更多的信息,我们只
需要使用类似于在系统调用中传递多于四个参数时所使用的技巧就可以了:我们通
过一个指向结构的指针将结果返回。收集系统资源使用情况的sysinfo系统调用就
是这种函数的一个样例。
24144:分配并清空一个struct sysinfo结构(15004行)以暂时存储返回值。
sys_sysinfo可以把结构中的每个域都独立地拷贝出来,但是这样会速度很慢、很
不方便,而且必然不容易阅读。
24148:禁止中断。这在第6章中会有详细的介绍;作为目前来说,我们只要说明这
种模式在使用的过程中能够确保sys_sysinfo正在使用的值不会改变就足够了。
24149:struct sysinfo结构的uptime域用来指明系统已经启动并运行了的秒数。
这个值是使用jiffies(26146行)和HZ来计算的。jiffies计算了系统运行过程中
时钟的滴答次数;HZ是系统相关的一个参数,它十分简单,就是每秒内部时钟滴答
的次数。
24151:数组avenrun(27116行)记录了运行队列的平均长度——也就是等待CPU的
平均进程数——在最后的1秒钟,5秒钟和15秒钟。calc_load(27135行)周期性的
重复计算它的值。由于内核中是要严格禁止浮点数运算的,所以只能通过计算变化
的次数这一修正值来计算。
24155:同样记录系统中当前运行的进程数。
24158:si_meminfo(07635行)写入这个结构中的内存相关成员,si_swapinfo(
38544行)写入与虚拟内存相关的部分。
24161:现在整个结构都已经全部填充了。sysinfo试图将其拷贝回用户空间,如果
失败就返回EFAULT,如果成功就返回0。


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

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


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

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