这篇文章给大家介绍Linux系统编程规范有哪些,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。
1.1 系统调用概述系统调用是操作系统内核提供给应用程序的基础接口,需要运行在操作系统的核心模式下,以确保有权限执行某些 CPU 特权指令。Linux 系统提供了功能非常丰富的系统调用,涵盖了文件操作、进程控制、内存管理、网络管理、套接字操作、用户管理、进程间通信等各个方面。执行如下命令,可列出系统中所有的系统调用名称。Linux 自带的 man 手册对每个系统调用都进行了非常详细的说明,包括函数功能、传入的参数、返回值,以及可能产生的错误、使用注意事项,等等,其完善程度丝毫不亚于微软的 MSDN。虽然是英文版,但读起来比较通俗易懂,每位 Linux 系统开发者都应该习惯于查看这些文档。另外,IBM 文档库里有一份质量非常高的《中文版系统调用列表》,阅读它会更轻松。1.2 系统调用的两种调用方式我们先看一种方式。系统调用由指派的编号来标识,通过 syscall 函数以编号为参数可直接被调用。syscall 函数原型为:完整的系统调用编号都定义在 sys/syscall.h 的文件中。感兴趣的读者可以自行查看。显然,记忆如此多的编号,对开发者很不友好。于是,开发者多会选择第二种方式,即利用 glibc 提供的包装函数将这些系统调用包装成名字自解释的函数。这个过程,包装函数并没有做太多额外工作,主要是检查参数,将它们拷贝到合适的寄存器中,接着调用指定标号的系统调用,之后再根据结果设置 errno,供应用程序检查执行结果,以及其他相关工作。两种调用方式,在功能上可以认为是完全等价的,但在易读、易用性上,glibc 包装函数则更有优势。在之后的课程中,我提到某系统调用,若无特殊说明,指的便是 glibc 包装函数。当然,如果包装函数无法满足某些特殊应用场景需求,还可以使用 syscall 函数直接执行系统调用。不过这种情况非常少见,到目前为止,我还没有遇到过。1.3 系统调用的两种执行过程1.3.1 基于中断方式系统调用的实现代码是内核代码的一部分。执行系统调用代码,首先需要将系统从用户模式切换到核心模式。早期的系统调用通过软中断实现模式的切换,而中断号属于系统稀缺资源,不可能为每个系统调用都分配一个中断号。在 Linux 的实现中,所有的系统调用共用 128 号中断(也就是大名鼎鼎的 int 0x80 ),其对应的中断处理程序是 system_call,所有的系统调用都会转到这个中断处理程序中。接着,system_call 会根据 EAX 传入的系统调用标号跳转并执行相应的系统调用程序。如果需要更多的参数,会依次用 EBX、ECX、EDX、EDI 进行传递。函数执行完成之后,会把结果放到 EAX 中返回给应用程序。由此可知,一次系统调用便会触发一次完整的中断处理过程。在每次中断处理过程中,CPU 都会从系统启动时初始化好的中断描述表中,取出该中断对应的门描述符,并判断门描述符的种类。在确认门描述符的级别(DPL)不比中断指令调用者的级别(CPL)低之后,再根据描述符的内容,将中断处理程序中可能用到的寄存器进行压栈保存。然后,执行权限提升,设置 CS 和 EIP 寄存器,以使 CPU 跳转到指定的系统调用的代码地址,并执行目标系统调用。1.3.2 基于 SYSENTER 指令再仔细审视基于中断方式的系统调用的执行过程,不难发现,前面很多处理过程都是固定的,其实很没必要,如门描述符级别检查、查找中断处理程序入口,等等。为了省去这些多余的检查,Intel 在 Pen开发云主机域名tium II CPU 中加入了新的 SYSENTER 指令,专门用来执行系统调用。该指令会跳过前面检查步骤,直接将 CPU 切换到特权模式,继而执行系统调用,同时还增加了几个专用寄存器辅助完成参数传递和上下文保存工作。另外,还相应地增加了 SYSEXIT 指令,用来返回执行结果,并切回用户模式。在 Linux 实现了 SYSENTER 方式的系统调用之后,就有人用 Pentium III 的机器对比测试了两种系统调用的效率。测试结果显示,与中断方式相比,SYSENTER 在用户模式下因省掉了级别检查类的操作,花费的时间大幅减少了 45% 左右;在核心模式下,因少了一个寄存器压栈保存动作,所花费的时间也减少了 2% 左右。目前,基于中断方式的系统调用仍然保留着,Linux 启动时会自动检测 CPU 是否支持 SYSENTER 指令,从而根据情况选择相应的系统调用方式。1.3.3 SYSENTER 指令诞生故事介绍完了 SYSENTER 指令的优越之处,我们回过头再来聊聊它的由来。从 Linux 2.5 内核开始,在经历了多方测试、多次 Patch 之后,SYSENTER 指令才正式被 Linux 2.6 版本支持,且由 Linus Torvalds 大神亲自操刀实现。上面提到过,其实早在 1998 年,SYSENTER 指令就已经引入到 Intel Pentium II CPU 中,直到 2002 年才在 Linux 2.5 版本的内核中出现。该指令一出现,Linux 社区就开始了激烈讨论。后来 Intel Pentium 4 CPU 发布了,这款 CPU 在“设计上存在的问题,造成 Pentium 4 使用中断方式执行系统调用比 Pentium 3 以及 AMD Athlon 所耗费的 CPU 时钟周期多 5~10 倍”,Linus 对这个结果接受不了,于是在 Linux 2.6 内核中加入了 SYSENTER 指令,从而实现了更加高效的系统调用。这里总结下系统调用的执行过程。进程从用户模式转入核心模式,开始执行内核中实现特定功能的代码段,执行完成后再切回用户模式,并把执行结果返回给调用进程。在 Linux 2.4 版本之前,主要利用中断方式实现核心模式的切换;Linux 2.6 及以后版本的内核中,可以利用更高效的 SYSENTER 指令实现。1.4 系统调用的标准使用方法前面提到,本课程所说的系统调用,默认是指 glibc 中的包装函数。这些函数会在执行系统调用前设置寄存器的状态,并仔细检查输入参数的有效性。系统调用执行完成后,会从 EAX 寄存器中获取内核代码执行结果。内核执行系统调用时,一旦发生错误,便将 EAX 设置为一个负整数,包装函数随之将这个负数去掉符号后,放置到一个全局的 errno 中,并返回 −1。若没有发生错误,EAX 将被设置为 0,包装函数获取该值后,并返回 0,表示执行成功,此时无需再设置 errno。综上,系统调用的标准使用方法可总结为:根据包装函数返回值的正负,确定系统调用是否成功。如果不成功,进一步通过 errno 确定出错原因,根据不同的出错原因,执行不同的操作;如果成功,则继续执行后续的逻辑。代码示例如下:大多数系统调用都遵循这一过程,errno 是一个整数,可以用 perror 或 strerror 获得对应的文字描述信息。不过,也有几个特殊的系统调用,和上述使用方法存在些许差异。比如,其中有个函数会在调用之前将 errno 重置为 0,调用后,通过检查 errno 判断执行是否成功。此类函数只有非常少数的几个,使用之前,看看帮助页,就知道如何使用了。系统调用的使用规范就介绍到这里。此时,你可能有个疑问,每个系统调用失败后都会设置 errno,如果在多线程程序中,不同线程中的系统调用设置的 errno 会不会互相干扰呢?如果 errno 是一个全局变量,答案是肯定的。如果真是这样的话,那系统调用的局限性也就太大了,总不能在每个系统调用之前都加锁保护吧。优秀的 Linux 肯定不会这么弱,那么,这个 errno 的问题又是怎么解决的呢?1.5 errno 的多线程问题根据 man 手册,要使用 errno,首先需要包含 errno.h 这个头文件。我们先看看 errno.h 里面有什么东西。执行以上代码,会发现该文件中有这样几行关键内容:根据官方提供的代码注释,bits/errno.h 中应该有一个 errno 的宏定义。如果没有,则会在外部变量中寻找一个名为 errno 的整数,它自然也就成了全局整数。否则,这个 errno 只是一个 per-thread 变量,每个线程都会拷贝一份。关于 per-thread 变量更详细的信息,我们会在后面的课程中介绍。现在,你只需知道,这个 errno,每个线程都会独立拷贝一份,所以在多线程程序中使用它是不会相互影响的。1.5.1 实现原理具体是怎么做到的呢?我们可以再打开 bits/errno.h 看一眼。原来,当 libc 被定义为可重入时,errno 就会被定义成一个宏,该宏调用外部 __errno_location 函数返回的内存地址中所存储的值。在 GCC 源码中,我们还发现一个测试用例中定义了 __errno_location 函数的 Stub,是这样写的:这一简单的测试用例充分展现了 errno 的实现原理。errno 被定义为 per-thread(用 __thread 标识的线程局部存储类型)变量 __libc_errno,之后 __errno_location 函数返回了这个线程局部变量的地址。所以,在每个线程中获取和设置 errno 的时候,操作的是本线程内的一个变量,不会与其他线程相互干扰。至于 __thread 这个关键字,需要在很“严苛”的条件下才能生效——需要 Linux 2.6 以上内核、pthreads 库、GCC 3.3 或更高版本的支持。不过,放到今天,这些条件已成为标配,也就不算什么了。1.5.2 注意事项上面只是解释了在多线程中使用系统调用时,errno 不会发生冲突问题,但并不是说所有的系统调用都可以放心大胆地在多线程程序中使用。有一些系统调用,标准中并没有规定它们的实现必须是多线程安全的(或者说可重入的,后面的课程中再详细解释)。由于历史原因和实现原理上的限制,有些函数的实现并不是线程安全的,比如 system()。某些 glibc 函数也是一样,比如 strerror 函数,其内部使用一块静态存储区存放 errno 描述性信息,最近的一次调用会覆盖上一次调用的内容。关于Linux系统编程规范有哪些就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。
相关推荐: DCDiag error after upgrading to DFS-R
After switching a domain to use DFS-R rather than FRS for SYSVOL replication you may experience the following error when running d…
免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。