系统调用简介

系统调用是受控的内核入口,借助于这一机制,进程可以请求内核以自己的名义去执行某些动作。以应用程序编程接口(API)的形式,内核提供有一系列服务供程序访问。这包括创建新进程执行I/O,以及为进程间通信创建管道等

在深入学习系统调用的运作方式之前,务必关注以下几点:

  • 系统调用将处理器从用户态切换到核心态,以便CPU访问受到保护的内核内存。
  • 系统调用的组成是固定的,每个系统调用都由一个唯一的数字来标识。(程序通过名称来标识系统调用,对这一编号方案往往一无所知。)
  • 每个系统调用可辅之以一套参数,对用户空间(亦即进程的虚拟地址空间)与内核空间之间(相互)传递的信息加以规范。

从编程角度来看,系统调用与C语言函数的调用很相似。然而,在执行系统调用时,其幕后会历经诸多步骤。为说明这点,下面以一个具体的硬件平台——X86-32为例,按事件发生的顺序对这些步骤加以分析。

  • 应用程序通过调用C语言函数库中的外壳(wrapper)函数,来发起系统调用。
  • 对系统调用中断处理例程(稍后介绍)来说,外壳函数必须保证所有的系统调用参数可用。通过堆栈,这些参数传入外壳函数,但内核却希望将这些参数置入特定寄存器。因此,外壳函数会将上述参数复制到寄存器
  • 由于所有系统调用进入内核的方式相同,内核需要设法区分每个系统调用。为此,外壳函数会将系统调用编号复制到一个特殊的CPU寄存器(%eax)中。
  • 外壳函数执行一条中断机器指令(int0x80),引发处理器从用户态切换到核心态,并执行系统中断0x80(十进制数128)的中断矢量所指向的代码。(较新的x86-32硬件平台实现了sysenter指令,较之传统的int 0x80中断指令,sysenter指令进入内核的速度更快。2.6内核及glibc 2.3.2以后的版本都支持sysenter 指令)
  • 为响应中断0x80,内核会调用system_call()例程(位于汇编文件arch/i386/entry.S中)来处理这次中断,具体如下。
    • 在内核栈中保存寄存器值 
    • 审核系统调用编号的有效性
    • 以系统调用编号对存放所有调用服务例程的列表(内核变量sys_call_table)进行索引,发现并调用相应的系统调用服务例程。若系统调用服务例程带有参数,那么将首先检查参数的有效性。例如,会检查地址指向用户空间的内存位置是否有效。随后,该服务例程会执行必要的任务,这可能涉及对特定参数中指定地址处的值进行修改,以及在用户内存和内核内存间传递数据(比如,在I/O操作中)。最后,该服务例程会将结果状态返回给system_call)例程。
    • 从内核栈中恢复各寄存器值,并将系统调用返回值置于栈中
    • 返回至外壳函数,同时将处理器切换回用户态
  • 若系统调用服务例程的返回值表明调用有误,外壳函数会使用该值来设置全局变量errno(参见3.4节)。然后,外壳函数会返回到调用程序,并同时返回一个整型值,以表明系统调用是否成功。

在Linux上,系统调用服务例程遵循的惯例是调用成功返回非负值。发生错误时,例程会对相应errmo常量取反,返回一负值。C语言函数库的外壳函数随即对其再次取反(负负得正),将结果拷贝至errmo,同时以-1作为外壳函数的返回值返回,向调用程序表明有错误发生。
上述惯例所依赖的前提条件是系统调用服务例程,若调用成功则不会返回负值。可是,对于少数例程来说,这一前提并不成立。一般情况下,这也不会有问题,因为取反的errno值范围不会与调用成功返回负值的范围有交集。不过,有一种情况,沿用这个惯例确实会出问题:系统调用fentl()的F_GETOWN操作,会在63.3节加以描述。

图3-1以系统调用execve()为例,展示了上文述及事件的发生序列。在Linux/x86-32上,execve()的系统调用号为11(NR_execve)。因此,在sys_call_table向量中,条目11包含了该系统调用的服务例程sys_execve()的地址。(在Linux中,系统调用服务例程的命名通常会采取sys_xyz()的形式,其中,xyz()正是所论及的系统调用。) 

 因此,从C语言编程的角度来看,调用C语言函数库的外壳(wrapper)函数等同于调用相应的系统调用服务例程,在本书后续内容中,“调用系统调用xyz()”这类说法就意味着“调用外壳函数,由外壳函数去调用系统调用xyz()”。

为调试程序,或是研究程序的运作机制,可使用附录A所介绍的strace命令,对程序发起的系统调用进行跟踪。

更多与Linux系统调用机制有关的信息请见[Love,2010]、[Bovet&Cesati,2005]以及[Maxwell,1999]。

 库函数

一个库函数是构成标准C语言函数库的众多库函数之一。(出于简化,本书后文提到某具体函数时,通常将其称为“函数”而非“库函数”。)库函数的用途多种多样,可用来执行以下任务:打开文件、将时间转换为可读格式,以及进行字符串比较等

许多库函数(比如,字符串操作函数)不会使用任何系统调用。另一方面,还有些库函数构建于系统调用层之上。例如,库函数fopen0就利用系统调用open0来执行打开文件的实际操作。往往,设计库函数是为了提供比底层系统调用更为方便的调用接口。例如,printfO函数可提供格式化输出和数据缓存功能,而write0系统调用只能输出字节块。同理,与底层的brk0系统调用相比,malloc0和free0函数还执行了各种登记管理工作,内存的释放和分配也因此而容易许多。

标准C语言函数库;GNUC语言函数库(glibc)

标准C语言函数库的实现随UNIX的实现而异。GNUC语言函数库(glibc,http://www.gnu.org/software/libc/)是Linux上最常用的实现。

最初,Roland McGrath是GNU C 语言函数库的主要开发者和维护者。如今,UIrich Drepper挑起了这副重担。
Linux同样支持各种其他 C 语言函数库,其中包括应用于嵌入式设备领域、受限内存条件下的C语言函数库。uClibc(http://www.uclibc.org/)和dietlibc(http://www.fefe.de/dietlibc/)便是其中的两个例子。本书的讨论范围仅限于glibc,因为Linux开发的大多数应用程序都使用该函数库。

确定系统的glibc版本 

有时,需要确定系统所安装的glibc版本。在shell中,可以直接运行glibc共享库文件——将其视为可执行文件——来获取glibc版本。这会输出各种文本信息,其中也包括了glibc的版本号。

[root@ecs tmp]# /lib64/libc.so.6 
GNU C Library (GNU libc) stable release version 2.17, by Roland McGrath et al.
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 4.8.5 20150623 (Red Hat 4.8.5-39).
Compiled on a Linux 3.10.0 system on 2019-08-06.
Available extensions:
	The C stubs add-on version 2.1.2.
	crypt add-on version 2.1 by Michael Glad and others
	GNU Libidn by Simon Josefsson
	Native POSIX Threads Library by Ulrich Drepper et al
	BIND-8.2.3-T5B
	RT using linux kernel aio
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<http://www.gnu.org/software/libc/bugs.html>.

在某些Linux发行版中,GNUC语言函数库的路径名并非“/lib/libc.so.6”。确定该库所在位置的方法之一是:针对某个与glibc动态链接的可执行文件(大多数可执行文件都采用这种链接方式),运行1dd(列出动态依赖性)程序。接下来,再检查输出的库依赖性列表,便能发现glibc共享库的位置:

[root@ecs tmp]# ldd /usr/bin/ls
    ...
	libc.so.6 => /lib64/libc.so.6 (0x00007fbb30e74000)
	...

应用程序可通过测试常量和调用库函数这两种方法,来确定系统所安装的glibc版本。从版本2.0开始,glibc定义了两个常量:_GLIBC_和_GLIBC_MINOR_,供程序在编译时(在#ifdef语句中)测试使用。在安装有glibc2.12版本的系统上,以上两个常量的值分别为2和12。然而,如果程序在A系统上编译,而在B系统(安装了不同版本的glibc)上运行,这两个常量作用就有限了。为应对这种可能,程序可以调用函数gnu get libc_version0,来确定运行时的glibc版本。

#include <gnu/libc-version.h>

const char *gnu_get_libc_version(void);

                    Returns pointer to null-terminated, statically allocated string
                    containing GNU C library version number

函数gnu_get_libc_version()返回一个指针,指向诸如“2.12”的字符串。

#include <gnu/libc-version.h>
#include <stdlib.h>
#include <stdio.h>

int
main(int argc, char *argv[])
{
    printf("GNU libc version: %s\n", gnu_get_libc_version());
    printf("GNU libc release: %s\n", gnu_get_libc_release());
    exit(EXIT_SUCCESS);
}

 编译并运行

[root@ecs tmp]# ./getglibc 
GNU libc version: 2.17
GNU libc release: stable

 参考gnu_get_libc_version(3) - Linux manual page

处理来自系统调用和库函数的错误

几乎每个系统调用和库函数都会返回某类状态值,用以表明调用成功与否。要了解调用是否成功,必须坚持对状态值进行检查。若调用失败,那么必须采取相应行动。至少,程序应该显示错误消息,警示有意想不到的事件发生。

不检查状态值,少敲几个字,听起来的确诱人(尤其是见识到了不检查状态值的UNIX/Linux程序以后),但实际却得不偿失。认定系统调用或库函数“不可能失败”,不对状态返回值进行检查,这会浪费掉大把的程序调试时间。

少数几个系统函数在调用时从不失败。例如,getpid()总能成功返回进程的ID,而_exit()总能终止进程。无需对此类系统调用的返回值进行检查。

 处理系统调用错误

每个系统调用的手册页记录有调用可能的返回值,并指出了哪些值表示错误。通常,返回值为-1表示出错。因此,可使用下列代码对系统调用进行检查:

fd = open(pathname, flags, mode); /* system call to open a file */
if (fd == -1) {
 /* Code to handle the error */
}
...
if (close(fd) == -1) {
 /* Code to handle the error */
}

系统调用失败时,会将全局整形变量errno设置为一个正值,以标识具体的错误。程序应包含<errno.h>头文件,该文件提供了对errno的声明,以及一组针对各种错误编号而定义的常量。所有这些符号名都以字母E打头。在每个手册页内标题为ERRORS的章节内,都刊载有一份相应系统调用可能返回的ermo值列表。以下便是利用errno 诊断系统调用错误的一个简单示例:

cnt = read(fd, buf, numbytes);
if (cnt == -1) {
 if (errno == EINTR)
 fprintf(stderr, "read was interrupted by a signal\n");
 else {
 /* Some other error occurred */
 }
}

查询系统调用详情的网站Linux manual pages: section 2

本文翻译自《The Linux Programming Interface》,参考Michael Kerrisk - man7.org

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值