第3章 Linux/UNIX 系统编程概念

第3章 系统编程概念

本章涉及到的多个主题是系统编程的预备知识。我们首先会介绍系统调用(system calls)以及在执行过程中的细节。然后我们会讲到库函数,以及它们与系统调用之间的区别,同时对C库(GNU)进行相关描述。
当我们使用系统调用或者调用一个库函数时,我们应该经常检查一下返回的状态,以确保是否成功返回了结果。我们会描述如何执行这些检查,并且给出一组函数,用于诊断示例程序中的系统调用和库函数执行是否正确。

3.1 System Calls

系统调用(system call)是内核的一个受控的切入点,进程通过该切入点可以向内核请求为它执行一些操作。通过系统调用应用编程接口(API),内核为程序提供了一系列可用的服务。这些服务包括,例如:创建一个新的进程、执行I/O操作、为进程间通信创建管道等。
在深入了解系统调用如何工作之前,我们注意到:

  • 系统调用将处理器从用户模式(user mode)切换到内核模式(kernel mode),因此CPU可以访问受保护的内核内存。
  • 系统调用的集合是固定的。每个系统调用都被唯一的一个数字所标识(这些数字标识对程序而言是不可见的,程序通过名称来识别系统调用)。
  • 每个系统调用可能存在一组参数,用于接收从用户空间 (user space,也就是进程的虚拟地址空间) 传到内核空间(kernel space)的信息。

从编程的角度看,调用一个系统调用看上去更像是调用了一个C函数。然而在后台,在系统调用执行的这个过程中,会产生很多步骤。为了说明这些,我们按顺序来描述发生的这些步骤。这里采用x86-32这个特定的硬件实现,步骤如下:

  1. 应用程序通过调用C库中的 包装函数(wrapper function) 来产生一个系统调用。
  2. 该包装函数必须传入处理trap的 系统调用例程(system call routine) 所需的所有参数。这些参数通过栈传入到包装函数中,但是内核希望这些参数在特定的寄存器(registers)中。所有包装函数将这些参数复制到寄存器中。
  3. 因为所有的系统调用以相同的方式进入内核,所以内核需要通过某些方法来识别这些系统调用。为了能让内核能够识别,包装函数需要将系统调用的标识符复制到特定的CPU寄存器中。
  4. 包装函数执行 trap 机器指令(int 0x80),该指令会使处理器从用户模式切换到内核模式,并执行系统trap向量中位置0x80所指向的代码。

较新的x86-32架构实现了 sysenter 指令,该指令与传统的 int 0x80 trap 指令相比,能够更快地切换到内核模式。从2.6内核(2.6 kernel)和glibc2.3.2开始支持sysenter指令。

  1. 为响应位置0x80的trap,内核调用system_call()例程来解决这个trap:
    a) 将寄存器的值保存到 内核栈(kernel stack) 中。
    b) 检查系统调用的标识符(number)的有效性。
    c) 根据 系统调用标识符系统调用列表(内核变量sys_call_table) (包含所有系统调用服务例程的列表) 中找到的相应的系统调用服务例程(system call service routine),并调用。如果该系统调用服务例程带有任何参数,那么首先会检查参数的有效性。例如,它会检查这个地址所指向的用户内存的位置是否有效。然后服务例程会执行这个请求的任务,如修改该参数所指向地址的值,从用户内存和内核内存之间传输数据(例如 I/O操作)。最后,服务例程将结果状态值返回给system_call()例程。
    d) 从内核栈中恢复寄存器的值,将系统调用(system call)返回的值放入栈中。
    e) 返回给包装函数,同时将处理器切换到用户模式。
  2. 如果系统调用服务例程返回的值是一个表示产生了错误(an error)的值,那么包装函数会将该值赋给全局变量 errno(正整数) 。然后从包装函数返回一个整型值给调用者(caller),告知这次系统调用是成功的还是失败的。

在Linux中,系统调用服务例程按照惯例,如果返回一个 非负数 则表示运行成功。如果出现了错误,则会返回一个 负数 ,这个数正好是errno常量的负值。当返回一个负数时,C库包装函数会对这个值再取负(使它变成正数),将结果赋给errno,并将-1作为包装函数的结果进行返回,告知调用程序产生了一个错误。

图3.1使用execve()系统调用描述了上面的一系列步骤。在Linux/x86-32中,execve()的系统调用标识符是11 (__NR_execve)。因此,在sys_call_table向量中,条目11包含了sys_execve()的地址,sys_execve()就是这个系统调用所需的服务例程。

在Linux中,系统调用服务例程 名称的形式一般是 sys_xyz() , xyz() 是 ++系统调用++ 的名称。

上面段落中描述的信息已经超过了我们所需要知道的细节。但是,它告诉我们,即使是一个简单的系统调用,其实有许多工作需要做,因此系统调用的开销虽然小但还是可以感知的。

举一个系统调用开销的例子,getppid() 这个系统调用仅仅是返回调用进程的父进程的ID。但是在装有Linux2.6.25的x86-32系统中,执行一次getppid()需要1000万次调用,大概耗时2.2秒才能完成。每次调用大概是0.3毫秒。相比之下,在相同系统中,对C函数进行1000万次调用仅仅只需0.11秒。大概只需要getppid()二十分之一的时间。当然,还有些系统调用的开销远远大于getppid()

因此,从C程序的角度看,调用C库包装函数跟调用相应的系统调用服务例程是差不多的。在本书的剩下内容中,出现类似“调用系统调用xyz()” (invoking the system call xyz()) 就是指“通过调用包装函数来调用系统调用xyz()” (calling the wrapper function that invokes the system call xyz())。

在这里插入图片描述

3.2 Library Functions

库函数(library function) 仅仅是众多函数中的一种,它们组成了标准的C库(为简洁起见,在本书的剩下章节中,我们用 函数(function) 指代 库函数(library function) )。这些函数的作用是多样的,例如打开文件、将时间转换成可读格式或者比较两个字符串。
很多库函数不会使用系统调用(例如字符串操作函数)。而有些库函数依赖于系统调用。例如,fope()库函数实际使用了open()系统调用来打开文件。通常,库函数设计成比系统调用使用起来更加方便。例如,printf() 函数提供了输出格式和数据缓存,而 write() 系统调用仅仅输出一些字节。类似地,使用 malloc()free() 函数比 brk() 系统调用可以更容易的分配和释放一块内存。

3.3 The Standard C Library; The GNU C Library (glibc)

在各种UNIX系统实现中,对标准C库的实现也是各不相同的。在Linux中,最常用的C库是 GNU C库 (GNU C library)(glibc,http://www.gnu.org/software/libc/)。

Determining the version of glibc on the system

有时,我们需要知道系统中glibc的版本。glibc是一个可执行的程序,所以我们可以在shell中运行glibc共享库文件来得到它的版本号:

$ /lib/libc.so.6

在有些Linux系统中,GNU C库不是在/lib/libc.so.6这个位置。确定该库位置的一种方法是运行 ldd() 打印出某个可执行程序所依赖的共享库列表(大部分程序都对libc.so.6有依赖):

$ ldd myprog | grep libc

ldd (list dynamic depedencies) 命令用于打印程序或者库文件所依赖的共享库列表。

我的测试结果如下:
在这里插入图片描述

# include <stdio.h>
# include <gnu/libc-version.h>
int main()
{
	printf("glibc's version: %s\n", gnu_get_libc_version());
	return 0;
}

3.4 Handling Errors from system Calls and Library Funtions

3.5 Notes on the Example Programs in This Book

3.6 Portability Issues

3.7 summary

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Shell是一种经典的LinuxUnix操作系统中的命令解释器。它是用户与操作系统内核交互的桥梁,用户可以通过Shell执行各种命令,管理文件和目录,运行程序等操作。 Shell在LinuxUnix系统中有多种不同的实现,其中最常用的是Bash(Bourne Again SHell)。Bash是一个功能强大的Shell,它支持历史命令记录、命令补全和脚本编程等特性,使得用户在操作系统中的交互更加便捷和高效。 除了Bash,还有其他一些常用的Shell,如Csh、Ksh和Tcsh等。每种Shell都有自己独特的特性和语法,用户可以根据自己的需求选择合适的Shell来使用。 Shell脚本编程是Shell的重要特性之一。通过编写Shell脚本,用户可以将一系列需要执行的命令和操作组合在一起,并可以通过变量、条件判断、循环等结构实现复杂的逻辑控制。这使得Shell脚本成为日常管理任务和自动化操作的首选工具。 除了命令解释器,Shell还可以用作软件开发中的自动化部署和执行工具。通过编写Shell脚本,开发人员可以自动化地进行编译、测试、打包和部署等繁琐的操作,提高开发效率和减少出错的可能性。 总之,Shell是LinuxUnix的经典命令解释器,通过Shell用户可以方便地与操作系统交互、管理文件和目录,并能够编写脚本实现自动化操作。它在系统管理、软件开发和日常操作中都扮演着重要的角色。 ### 回答2: Shell 是一种命令行解释器,用于与操作系统进行交互和执行命令。它是经典的 LinuxUnix 环境中常用的工具之一。 Shell 提供了一个用户界面,通过该界面可以输入系统命令并执行。用户可以使用 shell 提供的命令来处理文件、管理进程、操作系统等。Shell 还支持脚本编程,可以编写一系列命令的脚本来自动化任务。 在 LinuxUnix 操作系统中,有很多种不同的 shell。其中最常见的是 Bourne Shell (sh)、Bourne Again Shell (bash)、C Shell (csh) 和 Korn Shell (ksh)。这些不同的 shell 在语法和功能上有所不同,可以根据需要选择适合的 shell。 Shell 提供了很多方便的特性,例如命令历史记录,可以通过上下箭头键来重新执行之前执行过的命令;通配符和正则表达式,可以用来匹配文件名和文本;管道,可以将多个命令连接在一起形成一个命令序列;环境变量,用于保存系统和用户的配置信息等。 Shell 作为经典的 LinuxUnix 工具,被广泛应用于服务器管理、系统脚本、自动化任务等方面。通过编写 shell 脚本,可以简化重复性的工作,提高效率。同时,Shell 也是许多 Linux 发行版默认的命令行解释器,使得用户可以通过简单的命令来操作系统,而无需依赖图形界面。 总结而言,Shell 是经典的 LinuxUnix 环境下的命令行解释器,提供了一种与操作系统交互和执行命令的方式。它具有丰富的功能和特性,为用户提供了方便和灵活的操作方式,被广泛用于服务器管理和系统自动化等领域。 ### 回答3: Shell是一种经典的命令行界面,常用于操作LinuxUnix操作系统。它提供了一种与计算机交互的方式,通过输入命令来执行各种操作。Shell具有以下几个特点。 首先,Shell是一种脚本语言,它允许用户编写一系列的命令来完成一些复杂的任务。通过将多个命令组合在一个脚本中,用户可以快速地自动化执行一些常见的操作,大大提高了工作效率。 其次,Shell提供了丰富的命令和工具,用于管理和操作文件系统。用户可以使用命令来创建、复制、移动和删除文件,查看文件内容,改变文件的权限和所有权等。 此外,Shell还提供了强大的管道功能,允许用户将多个命令连接在一起,将一个命令的输出作为下一个命令的输入。这种灵活的管道机制使得用户可以通过组合多个简单的命令,实现复杂的数据处理和分析任务。 最后,Shell具有良好的扩展性和定制性。用户可以根据自己的需求,编写自定义的Shell脚本和函数,以及安装和使用第三方的Shell工具和插件。这使得Shell成为了一个高度可定制的工具,可以满足不同用户的不同需求。 总而言之,Shell是一种经典的命令行界面,它通过输入命令来操作LinuxUnix系统,并具备脚本编程、文件系统管理、管道功能和定制性等特点。无论是系统管理员、开发人员还是普通用户,都可以通过学习和使用Shell,轻松地进行各种操作和任务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值