您好,欢迎访问三七文档
本资料由-大学生创业|创业|创业网提供资料在线代理|网页代理|代理网页|减肥药排行榜|淘宝最好的减肥药|什么减肥药效果最好|减肥瘦身药|内核的书籍都没有仔细说明系统调用,我认为这是一个失误。实际上,我们实际需要的系统调用现在已经十分完美。因此,从某种意义上来说,研究系统调用的实现是无意义的——如果你想为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库中专门负责翻译的一个小层次(thinlayer)实现。)我们随便举一个例子,27825行本资料由-大学生创业|创业|创业网提供资料在线代理|网页代理|代理网页|减肥药排行榜|淘宝最好的减肥药|什么减肥药效果最好|减肥瘦身药|(sys_nanosleep的一部分)返回-EINVAL指明所提供的值越界了。标准C库中实际处理sys_nanosleep的代码会注意到返回的负值,从而设置errno和EINVAL,并且自己返回-1给原始的调用者。在最近的内核版本中,系统调用返回负值偶尔也不一定表示错误了。在目前的几个系统调用中(例如lseek),即使结果正确也会返回一个很大的负值。最近,错误返回值是在-1到-4095范围之内。现在,标准C库实现能够以更加成熟和高级的方式解释系统调用的返回值;当返回值为负时,内核本身就不用再做任何特殊的处理了。中断、内核空间和用户空间我们将在第6章中介绍中断和在第8章中介绍内存时再次明确这些概念。但是在本章中,我们只需要粗略地了解一些术语。第一个术语是中断(interrupt),它来源于两个方面:硬件中断,例如磁盘指明其中存放一些数据(这与本章无关);和软件中断,一种等价的软件机制。在x86系列CPU中,软件中断是用户进程通知内核需要触发系统调用的基本方法(出于这种目的使用的中断号是0x80,对于Intel芯片的研究者来说更为熟悉的是INT80)。内核通过system_call(171行)函数响应中断,这一点我们马上就会介绍。另外两个术语是内核空间(kernelspace)和用户空间(userspace),它们分别对应内核保留的内存和用户进程保留的内存。当然,多用户进程也经常同时运行,而且各个进程之间通常不会共享它们的内存,但是,任何一个用户进程使用的内存都称为用户空间。内核在某一个时刻通常只和一个用户进程交互,因此实际上不会引起任何混乱。由于这些内存空间是相互独立的,用户进程根本不能直接访问内核空间,内核也只能通过put_user(13278行)和get_user(13254行)宏和类似的宏才可以访问用户空间。因为系统调用是进程和进程所运行的操作系统之间的接口,所以系统调用需要频繁地和用户空间交互,因此这些宏也就会不时的在系统调用中出现。在通过数值传递参数的情况下并不需要它们,但是当用户把指针——内核通过这个指针进行读写——传递给系统调用时,就需要这些宏了。如何激活系统调用系统调用的的激活有两种方法:system_call函数和lcall7调用门(callgate)(请参看135行)。(你可能听说过还有一种机制,syscall函数,是通过调用lcall7实现的——至少在x86平台上是如此——因此,它并不是一个特有的方法。)本节将细致地讨论一下这两种机制。在阅读的过程中请注意系统调用本身并不关心它们是由system_call还是由lcall7激活的。这种把系统调用和其实现方式区别开来的方法是十分精巧的。这样,如果出于某种原因我们不得不增加一种激活系统调用的方法,我们也不必修改系统调用本身来支持这种方法。在你浏览这些汇编代码之前要注意这些机器指令中操作数的顺序和普通Intel的次序相反。虽然还有一些其它的语法区别,但是操作数反序是最令人迷惑的。如果你还记得Intel的语法:moveax,0(本句代码的意思是把常数0传送到寄存器EAX中)在这里应该写作:mov1$0,%eax这样你就能够正确通过。(内核使用的语法是AT&T的汇编语法。在GNU汇编文档中有更本资料由-大学生创业|创业|创业网提供资料在线代理|网页代理|代理网页|减肥药排行榜|淘宝最好的减肥药|什么减肥药效果最好|减肥瘦身药|多资料。)system_callsystem_call(171行)是所有系统调用的入口点(这是对于内部代码来说的;lcall7用来支持iBCS2,这一点我们很快就会讨论)。正如前面标题注释中说明的一样,目的是为普通情况简单地实现直接的流程,不采用跳转,因此函数的各个部分都是离散的——整体的流量控制已经因为要避免普通情况下的多分支而变得非常复杂。(分支的避免是十分值得的,因为它们引起的代价非常昂贵。它们可以清空CPU管道,使现存CPU的并行加速机制失效。)图5.1system_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_call172: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行的structtask_struct——这在第7章中将对它进行更详细的介绍。(更确切的描述是,%ebx的置换在structtask_struct中,%esp的置换在与structtask_struct相关联的structpt_regs结构中。但是这些细节在这里都并不重要。)175:检查(EAX中的)系统调用的数目是否超过系统调用的最大数量。(此处EAX为一个无符号数,因此不可能为负值。)如果的确超过了,就向前跳转到ba
本文标题:第5章系统调用
链接地址:https://www.777doc.com/doc-2196545 .html