本章是对系统编程的介绍,探讨了一系列主题,从最初的定义到它是如何随着系统演化而随时间变化的。本章提供一些基本概念和 Unix 及其资源的概述,包括内核和应用程序编程接口(API)。这些概念中的许多在这里定义,并在本书的其余部分中使用。
本章将介绍以下主题:
- 什么是系统编程?
- 应用程序编程接口
- 了解保护环的工作原理
- 系统调用概述
- POSIX 标准
如果您在 Linux 上,本章不要求您安装任何特殊软件。
如果您是 Windows 用户,您可以为 Linux 安装Windows 子系统(WSL。按照以下步骤安装 WSL:
- 以管理员身份打开 PowerShell 并运行以下操作:
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
- 出现提示时重新启动计算机。
- 从 Microsoft 应用商店安装您喜爱的 Linux 发行版。
多年来,IT 领域发生了巨大的变化。挑战冯·诺依曼机器、互联网和分布式系统的多核 CPU 只是过去 30 年中发生的一些变化。那么,系统编程在这种情况下处于什么地位呢?
让我们先从标准教科书的定义开始。
系统编程(或系统编程)是对计算机系统软件进行编程的活动。与应用程序编程相比,系统编程的主要区别在于,应用程序编程旨在生成直接向用户提供服务的软件(例如,字处理器),鉴于系统编程旨在生成为其他软件提供服务的软件和软件平台,并设计为在性能受限的环境中工作,例如操作系统、计算科学应用程序、游戏引擎和 AAA 视频游戏、工业自动化和软件即服务应用程序。
该定义强调了系统应用程序的两个主要概念,如下所示:
- 其他软件使用的软件,而不是最终用户直接使用的软件。
- 软件是硬件感知的(它知道硬件如何工作),并且面向性能。
这使得人们可以轻松地将操作系统内核、硬件驱动程序、编译器和调试器识别为系统软件,而不是系统软件、聊天客户端或文字处理器。
历史上,系统程序是使用汇编和 C 语言创建的。然后出现了 shell 和脚本语言,用于将系统程序提供的功能结合在一起。系统语言的另一个特点是控制内存分配。
在过去的十年中,脚本语言越来越流行,一些脚本语言的性能有了显著的提高,整个系统都是用它们构建的。例如,让我们考虑一下 JavaScript 的 V8 引擎和 Python 的 PyPy 实现,它们极大地改变了这些语言的性能。
其他语言(如 Go)证明了垃圾收集和性能并不是相互排斥的。特别是,Go 设法用 Go 在 1.5 版中编写的本机版本替换了自己用 C 编写的内存分配器,将其性能提高到了可比的程度。
与此同时,系统开始被分发,应用程序开始被装入容器中,由其他系统软件(如Kubernetes)协调。这些系统旨在维持巨大的吞吐量,并通过两种主要方式实现:
- 通过扩展来增加托管系统的计算机的数量或资源
- 通过优化软件以提高资源效率
在构建分布式系统时,系统编程的一些实践(例如,将应用程序绑定到硬件、面向性能以及在资源受限的环境中工作)也是一种有效的方法,其中,限制资源使用可以减少所需的实例数。看起来系统编程是解决一般软件工程问题的一种好方法。
这意味着,在构建任何类型的应用程序时,学习有关有效使用机器资源(从内存使用到文件系统访问)的系统编程概念将非常有用。
API 是用于构建软件的系列子例程定义、通信协议和工具。API 最重要的方面是它提供的功能,以及它的文档,这有助于用户在另一个软件中使用和实现软件本身。API 可以是允许应用软件使用系统软件的接口。
API 通常有一个特定的发布策略,用于特定的收件人组。这可以是以下内容:
- 私人及仅供内部使用
- 合作伙伴,仅可由确定的组使用。这可能包括希望将服务与其集成的公司
- 公共和可供每个用户使用
我们将看到有几种类型的 API,从用于使不同应用程序软件协同工作的 API,到操作系统向其他软件公开的内部 API。
API 可以指定如何连接应用程序和操作系统。例如,Windows、Linux 和 macOS 都有一个接口,使操作文件系统和文件成为可能。
与软件库相关的 API 描述并规定(提供如何使用它的说明)每个元素的行为,包括最常见的错误场景。API 的行为和接口通常被称为库规范,而库是该规范中描述的规则的实现。库和框架通常是语言绑定的,但是有一些工具可以使用不同语言的库。您可以使用 CGO 在 Go 中使用 C 代码,在 Python 中可以使用 CPython。
这使得使用特定的通信标准来操作远程资源成为可能,这些标准允许不同的技术协同工作,而不考虑语言或平台。一个很好的例子是Java 数据库连接(JDBC)API,它允许使用相同的函数集查询许多不同类型的数据库,或者 Java 远程方法调用 API(Java RMI),它允许像使用本地函数一样使用远程函数。
Web API 是定义一系列关于所用协议、消息编码和可用端点及其预期输入和输出值的规范的接口。这种 API-REST 和 SOAP 有两种主要模式:
- REST API 具有以下特征:
- 他们将数据视为一种资源。
- 每个资源都由一个 URL 标识。
- 操作类型由 HTTP 方法指定。
- SOAP 协议具有以下特征:
- 它们由 W3C 标准定义。
- XML 是用于消息的唯一编码。
- 他们使用一系列 XML 模式来验证数据。
保护环又称层次保护域,是用于保护系统不发生故障的机制。它的名称来源于其权限级别的层次结构,由同心环表示,当移动到外圈时,权限会减少。每个环之间都有特殊的门,允许外圈以受限的方式访问内圈资源。
环的数量和顺序取决于 CPU 体系结构。它们通常以递减权限进行编号,使环 0 成为最具权限的环。这适用于使用四个环(从环 0 到环 3)的 i386 和 x64 体系结构,但不适用于使用相反顺序(从 EL3 到 EL0)的 ARM。大多数操作系统没有使用所有四个级别;它们最终使用两级层次结构用户/应用程序(环 3)和内核(环 0)。
在操作系统下运行的软件将在用户(环 3)级别执行。为了访问机器资源,它必须与操作系统内核(在环 0 上运行)交互。下面列出了 ring 3 应用程序无法执行的一些操作:
- 修改确定当前环的当前段描述符
- 修改页表,防止一个进程看到其他进程的内存
- 使用 LGDT 和 LIDT 指令,防止它们注册中断处理程序
- 使用 I/O 指令,如 in 和 out,可以忽略文件权限并直接从磁盘读取
例如,对磁盘内容的访问将由内核进行中介,内核将验证应用程序是否具有访问数据的权限。这种协商可以提高安全性并避免失败,但会带来影响应用程序性能的重要开销。
有些应用程序可以设计为直接在硬件上运行,而无需操作系统提供的框架。这对于实时系统来说是正确的,在响应时间和性能方面没有任何妥协。
系统调用是操作系统为应用程序提供对资源的访问的方式。它是由内核实现的用于安全访问硬件的 API。
我们可以使用一些类别来划分操作系统提供的众多功能。其中包括对正在运行的应用程序及其流的控制、文件系统访问和网络。
这类服务包括load
,将程序添加到内存中,并在将控制权传递给程序本身之前准备执行;或execute
,在预先存在的进程上下文中运行可执行文件。属于此类别的其他操作如下所示:
end
和abort
-第一个要求应用程序退出,第二个强制退出。CreateProcess
,在 Unix 系统中也称为fork
,在 Windows 系统中也称为NtCreateProcess
。- 终止进程。
- 获取/设置进程属性。
- 等待时间、等待事件或信号事件。
- 分配和释放内存。
文件和文件系统的处理属于文件管理系统调用。有创建和删除文件,可以在文件系统中添加或删除条目;有open
和close
操作,可以控制文件以执行读写操作。还可以读取和更改文件属性。
设备管理处理除文件系统以外的所有其他设备,如帧缓冲区或显示。它包括从设备请求开始的所有操作,包括与设备之间的通信(读、写、寻道)及其释放。它还包括更改设备属性以及逻辑附加和分离设备属性的所有操作。
读取和写入系统日期和时间属于信息维护类别。此类别还负责其他系统数据,如环境。属于这里的另一组重要操作是进程、文件和设备属性的请求和操作。
从处理套接字到接受连接的所有网络操作都属于通信类。这包括连接的创建、删除和命名,以及s结束和接收消息。
Windows 有一系列不同的系统调用,涵盖所有内核操作。其中许多与 Unix 等价物完全对应。以下是一些重叠系统调用的列表:
| | 视窗 | Unix |
| 过程控制 | CreateProcess()
ExitProcess()
WaitForSingleObject()
| fork()
exit()
wait()
|
| 文件操作 | CreateFile()
ReadFile()
WriteFile()
CloseHandle()
| open()
read()
write()
close()
|
| 文件保护 | SetFileSecurity()
InitializeSecurityDescriptor()
SetSecurityDescriptorGroup()
| chmod()
umask()
chown()
|
| 设备管理 | SetConsoleMode()
ReadConsole()
WriteConsole()
| ioctl()
read()
write()
|
| 信息维护 | GetCurrentProcessID()
SetTimer()
Sleep()
| getpid()
alarm()
sleep()
|
| 通讯 | CreatePipe()
CreateFileMapping()
MapViewOfFile()
| pipe()
shmget()
mmap()
|
为了确保操作系统之间的一致性,IEEE 正式制定了一些操作系统标准。以下各节将对其进行说明。
Unix 的便携式操作系统接口(POSIX)代表了一系列操作系统接口标准。第一个版本可以追溯到 1988 年,涵盖了一系列主题,如文件名、shell 和正则表达式。
POSIX 定义了许多特性,它们被组织在四个不同的标准中,每个标准都关注 Unix 遵从性的不同方面。它们都被命名为 POSIX,后跟一个数字。
POSIX.1 是 1988 年的原始标准,最初命名为 POSIX,但后来重新命名,以便在不放弃名称的情况下为该系列添加更多标准。它定义了以下功能:
- 过程创建和控制
- 信号:
- 浮点异常
- 分段/内存冲突
- 非法指令
- 总线错误
- 计时器
- 文件和目录操作
- 管
- C 库(标准 C)
- I/O 端口接口和控制
- 进程触发器
POSIX.1b 专注于实时应用程序和需要高性能的应用程序。它着重于以下几个方面:
- 优先级调度
- 实时信号
- 钟表
- 信号量
- 消息传递
- 共享内存
- 异步和同步 I/O
- 内存锁定接口
POSIX.1c 引入了多线程范例,并定义了以下内容:
- 线程创建、控制和清理
- 线程调度
- 线程同步
- 信号处理
POSIX.2 将命令行解释器和实用程序的标准指定为cd
、echo
或ls
。
并非所有的操作系统都与 POSIX 兼容。例如,Windows 是在标准之后诞生的,它不符合标准。从认证的角度来看,macOS 比 Linux 更兼容,因为后者使用另一种构建在 POSIX 之上的标准。
大多数 Linux 发行版遵循Linux 标准库(LSB),这是另一个包含 POSIX 和更多内容的标准,重点是维护不同 Linux 发行版之间的互兼容性。由于开发人员没有进入认证过程,因此它不被认为是正式合规的。
然而,macOS 在 2007 年与雪豹发行版完全兼容,并从那时起通过 POSIX 认证。
Windows 不符合 POSIX 标准,但有许多尝试使其符合 POSIX 标准。有一些开源项目,如 Cygwin 和 MinGW,它们提供了一个不太符合 POSIX 的开发环境,并使用 Microsoft Visual C 运行时库支持 C 应用程序。微软自己也在 POSIX 兼容性方面做了一些尝试,比如微软 POSIX 子系统。微软最新推出的兼容层是 Windows Linux 子系统,这是一个可选功能,可以在 Windows 10 中激活,并且受到了开发人员(包括我自己)的欢迎。
在本章中,我们了解了系统编程意味着编写具有一些严格要求的系统软件,例如与硬件绑定、使用低级语言以及在资源受限的环境中工作。当构建通常需要优化资源使用的分布式系统时,它的实践可能非常有用。我们讨论了 API,即允许其他软件使用软件的定义,并列出了不同类型的 API—操作系统中的 API、库和框架,以及远程 API 和 web API。
我们分析了在操作系统中,如何在称为保护环的分层级别上安排对资源的访问,以防止不受控制的使用,从而提高安全性并避免应用程序出现故障。Linux 模型将这个层次结构简化为两个级别,分别称为用户和内核空间。所有的应用程序都在用户空间中运行,为了访问机器的资源,它们需要内核进行干预。
然后我们看到了一种称为系统调用的特定类型的 API,它允许应用程序向内核请求资源,并调解进程控制、文件访问和管理以及设备和网络通信。
我们概述了定义 Unix 系统互操作性的 POSIX 标准。在定义的功能中,还有 C API、CLI 实用程序、shell 语言、环境变量、程序退出状态、正则表达式、目录结构、文件名和命令行实用程序 API 约定。
在下一章中,我们将探讨 Unix 操作系统资源,如文件系统和 Unix 权限模型。我们将了解什么是流程,它们如何相互通信,以及它们如何处理错误。
- 应用程序和系统编程之间的区别是什么?
- 什么是 API?为什么 API 如此重要?
- 你能解释一下保护环是怎么工作的吗?
- 你能举例说明在用户空间中不能做的事情吗?
- 什么是系统调用?
- Unix 中使用哪些调用来管理进程?
- 为什么 POSIX 有用?
- Windows POSIX 兼容吗?