系统级编程是什么?
KEY:系统论 系统编程
System Programming
过去的Unix编程是没有系统不系统之分的。即便是开发 X Window也是在系统级(system-level)编程,看到系统的所有API。现代的操作系统编程有所谓[系统级编程 ],使用与[应用编程 ]不同的API(System programming API) 。
从编程的形式和耗费心力上,系统编程与应用编程没有本质区别,这也意味着一个经验丰富的应用程序员转向系统编程难度不大。系统编程与应用编程的不同在于:
- 第一,系统编程更接近硬件;系统程序员必须熟悉硬件环境和操作系统环境;相对的,应用程序员更多是熟悉应用的环境;
- 第二,系统编程使用的函数库和库函数调用方法与应用编程有一些不同。比如,在调用系统调用(syscall)时使用所谓的陷入方式,也就是软中断方式。
近年来,随计算应用深化,应用编程有远离系统编程的趋向。不过,这并不能说或者预言系统编程的末日的到来。因为,有人用Javascript或C#写应用就要有人写它的解释器和运行时。 此外,操作系统代码只能使用系统级编程。
本书的一些核心问题:
- 系统级接口(system-level interface)到底是什么?又如何编写Linux的系统级应用?
- 内核和C库具体提供了什么给我们?
- 如何编写优质代码?Linux又有什么已知的陷阱(tricks )?
- Linux的系统调用是如何实现的?
- What neat system calls are provided in Linux compared to other Unix variants? How does it all work? Those questions are at the center of this book.
Linux系统编程需要熟悉三大块内容:系统调用、C库和C编译器。
System Calls
[系统调用 ]就是用户空间与内核之间的函数接口,目的是为了给用户空间的程序请求内核服务和资源。与其它很多操作系统相比,Linux实现的系统调用少很多。比如,Linux为I386体系实现了300个左右的系统调用,而 Microsoft Windows据说实现数以千计的系统调用。Linux内核的不同平台实现在系统调用上存在差异,不过90%的系统调用是相同的。
Invoking system calls
出于安全性等因素,应用代码是不能够直接调用系统调用的。必须使用特殊的[陷入 ](trap)机制。一种“知会 ”内核进行工作的函数调用形式(KEMIN:证明两“系统”的耦合度较弱,比直接调用方式要弱。以系统论的角度考究syscall也很有意思)。陷入机制的具体实现也是因不同的体系而有所不同 的。比如I386体系,应用代码通过触发软中断指令(int 0x80)来调用syscall。那0x80是什么呢?软件中断向量号吗?回答否。应用代码必须通过处理器的寄存器向告诉内核向量号和调用参数。比如,如果应用代码调用open(),它得置eax值5,然后把参数放在另外的五个寄存器:ebx, ecx, edx, esi, 和edi(所以系统调用至多使用五个参数),这些寄存器保存有用户空间的地址,也就是参数数据所在。
作为一位系统程序员,你一般不需干涉系统调用的过程,因为调用过程由体系定义,并且由C库和C编译器自动处理 。
The C Library
C库是所有UINX应用的核心,因为无论你使用什么语言,你的代码最终还是调用C库。其它高级语言的库都是基于C库构建的,或者说是这些库是对C库的包装。现在的Linux,使用的C库是GNU libc,行话glibc。glibc不仅仅是个程序语言库,比如C标准库,它还是一个系统库,而且是一个现代操作系统库,函数涵盖了对系统调用的包装、线程支持、网络支持等。
The C Compiler
Linux的C编译器是gcc,过去gcc代表GNU C Compiler,是cc的在GNU项目的实现;现在gcc代表GNU Compiler Collection,不过gcc 仍然是C编译器的入口。Unix系统(包括Linux)使用的编译器与系统编程是高度相关的,因为编译器负责实现了C标准和系统ABI 。
APIs and ABIs
无人不希望自己写的程序具有很好的移植性(portability),可以运行在不同软件平台(如操作系统或应用框架)、硬件平台(如处理器体系及载板),甚至跨平台的开发版本运行。有多种因素影响着程序的可移植性,当中就有两组不同的[系统接口]影响程序的可移植性:第一组是应用编程接口(API),另一组是应用二进制接口(ABI)。
APIs
API是两支软件在源代码级的接口。通过这个标准的接口(一般以函数形式实现),客户代码(一般称高级别的软件代码)可以调用服务代码(低级别的软件代码)。API本身是抽象的,它仅定义了一个接口,不涉入应用程序如何实现的细节。
系统论里的接口范畴
接口或者端口是两子系统边界[信息交换]的规格或约定方式,用通俗的理解就是,信息是什么样的。接口是信息的格式。
应用编程接口,就是软件系统不同组成部分衔接的约定。由于近年来软件的规模日益庞大,常常会需要把复杂的系统划分成小的组成部分,编程接口的设计十分重要。程序设计的实践中,编程接口的设计首先要使系统的职责得到合理划分。良好的接口设计可以降低系统各部分的相互依赖,提高组成单元的内聚性,降低组成单元间的耦合程度,从而提高系统的维护性和扩展性。
由于API是抽象的,必须清晰区别接口定义与接口的实现。比如[C标准库]是API,uclibc是一个实现;POSIX 是API,glibc是一个实现。
那么API一般涵盖什么样的函数呢? 这是一个很有意思的问题。比如C标准库是一种语言库,它必须非常通用,所以接口函数不能依赖软件或硬件特性;相反POSIX 是操作系统标准,它相对没那么的通用。
ABIs
API是源代码级别接口,是逻辑约定;而ABI是二进制级别接口,定义的在特定的架构上两个软件模块之间的接口的物理实现方式。这种[物理实现约定 ]保证二进制代码兼容,也就是保证一段目标代码能够在任何具有同样ABI的系统上都正常运作,不需要重新编译源代码。
ABI([物理实现约定]) 的内容包括调用约定(calling conventions)、字节序(byte ordering)、寄存器使用(register use)、系统调用实现方式、对象链接、库行为和二进制格式。以调用规则为例,它规定了函数如何被调用,参数如何传递,哪些寄存器被保留和哪些会被破坏,以及调用者如何提取返回的结果。
尽管曾经尝试着为特定架构下不同的操作系统(特别是i386上的Unix操作系统)定义唯一的ABI,然而到目前为止还没有取得成效。相反,包括Linux在内的操作系统都尝试定义各自独立的ABI,这些ABI和架构紧密相连。大部分的ABI涉及了机器级别的概念,如特定的寄存器或者汇编指令。因此,在Linux系统中,每一个机器架构都有自己的ABI集合,事实上,我们以机器架构的名称来称呼这些ABI,例如alpha x86-64等。(nakeman: http://blog.csdn.net/keminlau/archive/2009/11/26/4874732.aspx)
1.4 Linux编程概念、POSIX和系统库
所有的Unix系统,包括Linux系统,都提供了一个共同的抽象和接口集合,这个共同点定义了Unix 。如对文件和进程的抽象、管道和套接字管理的接口等等,都是Unix的核心内容。