加入收藏 | 设为首页 | 会员中心 | 我要投稿 源码门户网 (https://www.92codes.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 服务器 > 搭建环境 > Windows > 正文

攻克Linux系统编程,细说系统调用规范,入行要先熟悉套路

发布时间:2019-03-17 17:25:37 所属栏目:Windows 来源:佚名
导读:副标题#e# 本文主要带大家深入研究 Linux 系统编程。系统编程的任务,可以定义为使用系统提供的功能解决我们面对的实际问题,而系统调用,则是系统开放给应用执行特定功能的接口。本文首先从 Linux 系统调用讲起,主要包括以下内容: 系统调用概述 系统调用

最后总结下系统调用的执行过程。进程从用户模式转入核心模式,开始执行内核中实现特定功能的代码段,执行完成后再切回用户模式,并把执行结果返回给调用进程。在 Linux 2.4 版本之前,主要利用中断方式实现核心模式的切换;Linux 2.6 及以后版本的内核中,可以利用更高效的 SYSENTER 指令实现。

1.4 系统调用的标准使用方法

前面提到,本课程所说的系统调用,默认是指 glibc 中的包装函数。这些函数会在执行系统调用前设置寄存器的状态,并仔细检查输入参数的有效性。系统调用执行完成后,会从 EAX 寄存器中获取内核代码执行结果。

内核执行系统调用时,一旦发生错误,便将 EAX 设置为一个负整数,包装函数随之将这个负数去掉符号后,放置到一个全局的 errno 中,并返回 −1。若没有发生错误,EAX 将被设置为 0,包装函数获取该值后,并返回 0,表示执行成功,此时无需再设置 errno。

综上,系统调用的标准使用方法可总结为:根据包装函数返回值的正负,确定系统调用是否成功。如果不成功,进一步通过 errno 确定出错原因,根据不同的出错原因,执行不同的操作;如果成功,则继续执行后续的逻辑。代码示例如下:

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

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

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

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

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

1.5 errno 的多线程问题

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

  1. vim /usr/include/errno.h 

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

  1. #include <bits/errno.h> 
  2. ....... 
  3. #ifndef errno 
  4. extern int errno; 
  5. #endif 

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

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

1.5.1 实现原理

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

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

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

  1. extern __thread int __libc_errno __attribute__ ((tls_model ("initial-exec"))); 
  2. int * __errno_location (void) 
  3.  return &__libc_errno; 

这一简单的测试用例充分展现了 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 描述性信息,最近的一次调用会覆盖上一次调用的内容。

(编辑:源码门户网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!