本文主要带你深入学习Linux系统编程。系统编程的任务可以定义为利用系统提供的功能来解决我们面临的实际问题,而系统调用则是系统向应用程序开放的实现特定功能的接口。本文从Linux系统调用入手,主要包括以下内容:系统调用概述系统调用的两种调用方式系统调用的两种执行过程系统调用的标准使用方法另外会扩展两个知识点:和早期的Linux与2.6及以后版本的内核相比,如何实现更高效的系统调用?globalerrno如何解决多线程冲突的问题?1.1系统调用概述系统调用是操作系统内核向应用程序提供的基本接口。在操作系统的内核模式下,保证有执行某些CPU特权指令的权限。Linux系统提供了一套非常丰富的系统调用,涵盖了文件操作、进程控制、内存管理、网络管理、socket操作、用户管理、进程间通信等各个方面。执行以下命令列出系统中所有的系统调用名称。mansyscallsLinux自带的man手册,对每一个系统调用都有非常详细的描述,包括函数功能、传入参数、返回值、可能出现的错误、使用注意事项等等,其完善程度不亚于微软的MSDN。虽然是英文的,但是比较容易阅读,每个Linux系统开发者都应该习惯看这些文档。另外,在IBM的文档库中有一个非常高质量的《中文版系统调用列表》,阅读起来更方便。1.2系统调用的两种调用方式先来看一种方法。系统调用由分配的编号标识,可以通过syscall函数将编号作为参数直接调用。syscall函数的原型为:intsyscall(intnumber,...);完整的系统调用号在sys/syscall.h文件中定义。有兴趣的读者可以自行查阅。显然,死记硬背这么多数字对开发者来说并不友好。因此,开发者往往会选择第二种方式,即利用glibc提供的包装函数,将这些系统调用包装成具有自解释名称的函数。在这个过程中,wrapper函数并没有做太多额外的工作,主要是检查参数,拷贝到合适的寄存器中,然后调用指定标签的系统调用,然后根据结果设置errno,供应用程序调用检查执行结果等相关工作。两种调用方式在功能上可以认为是完全等价的,只是glibcwrapper函数在可读性和易用性上更有优势。在后面的课程中,我提到的系统调用,如无特殊说明,均指glibcwrapper函数。当然,如果wrapper函数不能满足一些特殊应用场景的要求,也可以使用syscall函数直接执行系统调用。不过,这种情况非常少见,目前为止,我还没有遇到过。1.3系统调用的两个执行过程1.3.1基于中断的系统调用实现代码是内核代码的一部分。要执行系统调用代码,首先需要将系统从用户态切换到内核态。早期的系统调用是通过软中断实现模式切换的,而中断号是系统的稀缺资源,不可能为每个系统调用分配一个中断号。在Linux的实现中,所有的系统调用共享128号中断(也就是著名的int0x80),对应的中断处理程序是system_call,所有的系统调用都会转到这个中断处理程序中。然后system_call会根据EAX传入的系统调用标号跳转执行相应的系统调用程序。如果需要更多的参数,会使用EBX、ECX、EDX、EDI依次传递。函数执行后,结果会放入EAX中返回给应用程序。可以看出,一个系统调用会触发一个完整的中断处理过程。在每次中断处理过程中,CPU都会从系统启动时初始化的中断描述表中取出该中断对应的门描述符,并判断门描述符的类型。在确认门描述符的级别(DPL)不低于中断指令的调用者的级别(CPL)后,根据描述符的内容,将可能在中断处理程序中使用的寄存器推送到用于存储的堆栈。然后进行提权,设置CS和EIP寄存器,使CPU跳转到指定系统调用的代码地址,执行目标系统调用。1.3.2基于SYSENTER指令,仔细考察基于中断方式的系统调用的执行过程。不难发现,前面的很多处理流程都是固定的,其实是没有必要的,比如检查gatedescriptor的level,找到interrupthandler的入口。等等。为了省去这些多余的检查,Intel在PentiumIICPU中增加了一条新的SYSENTER指令,专门用来执行系统调用。该指令跳过前面的检查步骤,直接将CPU切换到特权模式,然后执行系统调用。同时增加了几个特殊的寄存器,辅助完成参数传递和上下文保存。另外对应增加了SYSEXIT指令,用于返回执行结果,切换回用户态。Linux实现了SYSENTER系统调用后,有人用PentiumIII的机器对比测试了这两个系统调用的效率。测试结果表明,与中断方式相比,由于省略了电平检查操作,SYSENTER在用户态的耗时大大减少了约45%;所用时间也减少了约2%。目前,基于中断方式的系统调用还是保留的。Linux在启动时会自动检测CPU是否支持SYSENTER指令,从而根据情况选择相应的系统调用方式。1.3.3SYSENTER命令的诞生故事介绍完SYSENTER命令的优点,我们再回头说说它的由来。从Linux2.5内核开始,经过多次测试和补丁,Linux2.6版本正式支持SYSENTER命令,并由LinusTorvalds亲自实现。如上所述,其实早在1998年,SYSENTER指令就已经被引入到IntelPentiumIICPU中,直到2002年才出现在Linux2.5内核中。这条指令一出现,Linux社区就开始了一场热烈讨论。后来发布了IntelPentium4CPU。该CPU存在“设计问题,导致Pentium4使用中断来执行比Pentium3和AMDAthlon多5到10倍的CPU时钟周期的系统调用”。Linus对这个结果表示无法接受,因此在Linux2.6内核中加入了SYSENTER指令,以实现更高效的系统调用。这里总结一下系统调用的执行过程。进程从用户态转移到内核态,开始执行内核中实现特定功能的代码段,执行完成后切换回用户态,并将执行结果返回给调用进程。在Linux2.4版本之前,中断模式主要用于切换内核模式;在Linux2.6及以后版本的内核中,可以使用更高效的SYSENTER指令来实现。1.4系统调用的标准用法前面提到,本课程中提到的系统调用默认指的是glibc中的wrapper函数。这些函数设置寄存器的状态并在执行系统调用之前仔细检查输入参数的有效性。系统调用执行完毕后,会从EAX寄存器中获取内核代码执行结果。当内核执行系统调用时,一旦发生错误,EAX被设置为一个负整数,wrapping函数将负数去掉符号,放入一个全局的errno中,并返回-1。如果没有错误发生,EAX会被设置为0。wrapper函数获取到值后,返回0,表示执行成功。这个时候不需要设置errno。综上所述,使用系统调用的标准方法可以归纳为:根据包装函数返回值的正负来判断系统调用是否成功。如果不成功,通过errno进一步判断错误原因,根据不同的错误原因进行不同的操作;成功则继续执行后续逻辑。代码示例如下:intret=syscallx(...);if(ret<0){//有错误,通过errno判断错误原因,进行不同的操作}else{//调用成功,继续工作}big大部分系统调用都遵循这个流程,errno是一个整数,可以使用perror或者strerror获取对应的文字描述信息。不过也有几个比较特殊的系统调用,与上面的使用方法略有不同。例如,其中一个函数在调用前将errno重置为0,调用后检查errno判断是否执行成功。这样的功能只有几个。在使用它们之前,请阅读帮助页面以了解如何使用它们。这里介绍系统调用的使用规范。说到这里,你可能会有一个疑问。每次系统调用失败后都会设置Errno。如果在多线程程序中,不同线程中系统调用设置的errno会不会互相干扰?如果errno是一个全局变量,答案是肯定的。如果是这样的话,那么系统调用的局限性就太大了,不可能在每次系统调用前都加上锁保护。优秀的linux肯定没有那么弱,那么这个errno问题怎么解决呢?1.5errno的多线程问题根据man手册,要使用errno,首先需要包含头文件errno.h。我们先看看errno.h里有什么。vim/usr/include/errno.h执行上面的代码,你会发现文件中有几行关键内容:#include
