温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Linux系统编程规范有哪些

发布时间:2021-06-24 14:38:45 来源:亿速云 阅读:193 作者:Leah 栏目:系统运维

这篇文章给大家介绍Linux系统编程规范有哪些,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。


1.1 系统调用概述

系统调用是操作系统内核提供给应用程序的基础接口,需要运行在操作系统的核心模式下,以确保有权限执行某些 CPU 特权指令。

Linux 系统提供了功能非常丰富的系统调用,涵盖了文件操作、进程控制、内存管理、网络管理、套接字操作、用户管理、进程间通信等各个方面。

执行如下命令,可列出系统中所有的系统调用名称。

man syscalls

Linux 自带的 man  手册对每个系统调用都进行了非常详细的说明,包括函数功能、传入的参数、返回值,以及可能产生的错误、使用注意事项,等等,其完善程度丝毫不亚于微软的  MSDN。虽然是英文版,但读起来比较通俗易懂,每位 Linux 系统开发者都应该习惯于查看这些文档。

另外,IBM 文档库里有一份质量非常高的《中文版系统调用列表》,阅读它会更轻松。

1.2 系统调用的两种调用方式

我们先看一种方式。

系统调用由指派的编号来标识,通过 syscall 函数以编号为参数可直接被调用。

syscall 函数原型为:

int syscall(int number, ...);

完整的系统调用编号都定义在 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 在 Pentium 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  确定出错原因,根据不同的出错原因,执行不同的操作;如果成功,则继续执行后续的逻辑。代码示例如下:

int ret = syscallx(...); if(ret < 0) {  //有错误了,通过 errno 确定出错的原因,执行不同的操作 } else {  //调用成功,继续干活 }

大多数系统调用都遵循这一过程,errno 是一个整数,可以用 perror 或 strerror 获得对应的文字描述信息。

不过,也有几个特殊的系统调用,和上述使用方法存在些许差异。比如,其中有个函数会在调用之前将 errno 重置为 0,调用后,通过检查 errno  判断执行是否成功。此类函数只有非常少数的几个,使用之前,看看帮助页,就知道如何使用了。

系统调用的使用规范就介绍到这里。此时,你可能有个疑问,每个系统调用失败后都会设置 errno,如果在多线程程序中,不同线程中的系统调用设置的 errno  会不会互相干扰呢?

如果 errno 是一个全局变量,答案是肯定的。如果真是这样的话,那系统调用的局限性也就太大了,总不能在每个系统调用之前都加锁保护吧。优秀的 Linux  肯定不会这么弱,那么,这个 errno 的问题又是怎么解决的呢?

1.5 errno 的多线程问题

根据 man 手册,要使用 errno,首先需要包含 errno.h 这个头文件。我们先看看 errno.h 里面有什么东西。

vim /usr/include/errno.h

执行以上代码,会发现该文件中有这样几行关键内容:

#include <bits/errno.h> ....... #ifndef errno extern int errno; #endif

根据官方提供的代码注释,bits/errno.h 中应该有一个 errno 的宏定义。如果没有,则会在外部变量中寻找一个名为 errno  的整数,它自然也就成了全局整数。否则,这个 errno 只是一个 per-thread 变量,每个线程都会拷贝一份。

关于 per-thread 变量更详细的信息,我们会在后面的课程中介绍。现在,你只需知道,这个  errno,每个线程都会独立拷贝一份,所以在多线程程序中使用它是不会相互影响的。

1.5.1 实现原理

具体是怎么做到的呢?我们可以再打开 bits/errno.h 看一眼。

<bits/errno.h> # ifndef __ASSEMBLER__ extern int *__errno_location (void) __THROW __attribute__ ((__const__)); # if !defined _LIBC || defined _LIBC_REENTRANT # define errno (*__errno_location ()) # endif # endif

原来,当 libc 被定义为可重入时,errno 就会被定义成一个宏,该宏调用外部 __errno_location 函数返回的内存地址中所存储的值。在  GCC 源码中,我们还发现一个测试用例中定义了 __errno_location 函数的 Stub,是这样写的:

extern __thread int __libc_errno __attribute__ ((tls_model ("initial-exec"))); int * __errno_location (void) {  return &__libc_errno; }

这一简单的测试用例充分展现了 errno 的实现原理。errno 被定义为 per-thread(用 __thread 标识的线程局部存储类型)变量  __libc_errno,之后 __errno_location 函数返回了这个线程局部变量的地址。所以,在每个线程中获取和设置 errno  的时候,操作的是本线程内的一个变量,不会与其他线程相互干扰。

至于 __thread 这个关键字,需要在很“严苛”的条件下才能生效&mdash;&mdash;需要 Linux 2.6 以上内核、pthreads 库、GCC 3.3  或更高版本的支持。不过,放到今天,这些条件已成为标配,也就不算什么了。

1.5.2 注意事项

上面只是解释了在多线程中使用系统调用时,errno 不会发生冲突问题,但并不是说所有的系统调用都可以放心大胆地在多线程程序中使用。

有一些系统调用,标准中并没有规定它们的实现必须是多线程安全的(或者说可重入的,后面的课程中再详细解释)。由于历史原因和实现原理上的限制,有些函数的实现并不是线程安全的,比如  system()。某些 glibc 函数也是一样,比如 strerror 函数,其内部使用一块静态存储区存放 errno  描述性信息,最近的一次调用会覆盖上一次调用的内容。

关于Linux系统编程规范有哪些就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI