系统调用
系统调用是操作系统内核提供给应用程序的基础插口,须要运行在操作系统的核心模式下,以确保有权限执行个别CPU特权指令。
Linux系统提供了功能极其丰富的系统调用,囊括了文件操作、进程控制、内存管理、网络管理、套接字操作、用户管理、进程间通讯等各个方面。执行如下命令linux基础教程,可列举系统中所有的系统调用名称。
mansyscalls
Linux自带的man指南对每位系统调用都进行了十分详尽的说明,包括函数功能、传入的参数、返回值,以及可能形成的错误、使用注意事项,等等,其建立程度丝毫不亚于谷歌的MSDN。其实是中文版,但读上去比较浅显易懂,每个Linux系统开发者都应当习惯于查看那些文档。
系统调用的2种调用方法
第一种形式:系统调用由委派的编号来标示,通过syscall函数以编号为参数可直接被调用。
syscall函数原型为:
intsyscall(intnumber,…);
缺点:记忆这么多的编号,对开发者很不友好。
第二种形式:借助glibc提供的包装函数将这种系统调用包装成名字自解释的函数.
包装函数并没有做太多额外工作,主要是检测参数,将它们拷贝到合适的寄存器中,接着调用指定标号的系统调用查看linux操作系统版本的命令,然后再按照结果设置errno,供应用程序检测执行结果,以及其他相关工作。
系统调用的2种执行过程
第一种中断形式:系统调用的实现代码是内核代码的一部份。执行系统调用代码,首先须要将系统从用户模式切换到核心模式。
初期的系统调用通过软中断实现模式的切换,而中断号属于系统稀缺资源,不可能为每位系统调用都分配一个中断号。
在Linux的实现中,所有的系统调用共用128号中断(也就是大名鼎鼎的int0x80),其对应的中断处理程序是system_call,所有的系统调用还会转回这个中断处理程序中。接着,system_call会依照EAX传入的系统调用标号跳转并执行相应的系统调用程序。假如须要更多的参数,会依次用EBX、ECX、EDX、EDI进行传递。函数执行完成以后,会把结果放在EAX中返回给应用程序。
因而一次系统调用便会触发一次完整的中断处理过程。在每次中断处理过程中,CPU就会从系统启动时初始化好的中断描述表中,取出该中断对应的门描述符,并判定门描述符的种类。在确认门描述符的级别(DPL)不比中断指令调用者的级别(CPL)低以后,再依照描述符的内容,将中断处理程序中可能用到的寄存器进行压栈保存。最后执行权限提高,设置CS和EIP寄存器,以使CPU跳转到指定的系统调用的代码地址,并执行目标系统调用。
第二种形式:基于SYSENTER指令
基于中断形式的系统调用的执行过程,不难发觉,上面好多处理过程都是固定的,虽然很没必要,如门描述符级别检测、查找中断处理程序入口,等等。
为了省去那些多余的检测,Intel在PentiumIICPU中加入了新的SYSENTER指令,专门拿来执行系统调用。
该指令会跳过上面检测步骤,直接将CPU切换到特权模式,从而执行系统调用,同时还降低了几个专用寄存器辅助完成参数传递和上下文保存工作。另外,还相应地降低了SYSEXIT指令,拿来返回执行结果,并切回用户模式。
在Linux实现了SYSENTER形式的系统调用以后RED HAT LINUX 9.0,就有人用PentiumIII的机器对比测试了两种系统调用的效率。测试结果显示,与中断方法相比,SYSENTER在用户模式下因省掉了级别检测类的操作,耗费的时间急剧降低了45%左右;在核心模式下,因少了一个寄存器压栈保存动作,所耗费的时间也降低了2%左右。
基于中断形式的系统调用依然保留着,Linux启动时会手动检查CPU是否支持SYSENTER指令,因而依据情况选择相应的系统调用方法。
系统调用的使用方式
glibc中的包装函数,这种函数会在执行系统调用前设置寄存器的状态,并仔细检测输入参数的有效性。系统调用执行完成后,会从EAX寄存器中获取内核代码执行结果。
内核执行系统调用时,一旦发生错误,便将EAX设置为一个负整数,包装函数骤然将这个正数除去符号后,放置到一个全局的errno中,并返回−1。若没有发生错误,EAX将被设置为0,包装函数获取该值后,并返回0,表示执行成功,此时无需再设置errno。
综上,系统调用的标准使用方式可总结为:依照包装函数返回值的正负查看linux操作系统版本的命令,确定系统调用是否成功。若果不成功,进一步通过errno确定出错缘由,依据不同的出错缘由,执行不同的操作;若果成功,则继续执行后续的逻辑。
大多数系统调用都遵守这一过程,errno是一个整数,可以用perror或strerror获得对应的文字描述信息。
问题;
每位系统调用失败后就会设置errno,假如在多线程程序中,不同线程中的系统调用设置的errno会不会相互干扰呢?
errno的多线程问题
要使用errno,首先须要包含errno.h这个头文件。我们先瞧瞧errno.h上面有哪些东西。
#include
…….
#ifndeferrno
externinterrno;
#endif
按照官方提供的代码注释,bits/errno.h中应当有一个errno的宏定义。若果没有,则会在外部变量中找寻一个名为errno的整数,它自然也就成了全局整数。否则,这个errno只是一个per-thread变量,每位线程就会拷贝一份。
因而errno,每位线程就会独立拷贝一份,所以在多线程程序中使用它是不会互相影响的。
里面只是解释了在多线程中使用系统调用时,errno不会发生冲突问题,但并不是说所有的系统调用都可以放心大胆地在多线程程序中使用。有一些系统调用,标准中并没有规定它们的实现必须是多线程安全的或则是可重入的。有些函数的实现并不是线程安全的,例如system(),个别glibc函数也是一样,例如strerror函数,其内部使用一块静态储存区储存errno描述性信息,近来的一次调用会覆盖上一次调用的内容。
glibc还额外为一些函数提供了多线程安全实现版本例如一些时间操作类的函数。大多数是在原函数名后加上_r后缀。