Linux系统C++程序设计1-Linux系统和POSIX 标准入门

发布时间 2023-12-12 09:12:17作者: 磁石空杯

1 Linux系统和POSIX 标准入门

本书介绍了Linux以及我们如何在Linux环境中使用C++来管理关键资源。我们想花一些时间在本章中加深对操作系统(OS)的基本了解。 您将更多地了解一些特定技术、系统调用接口和可移植操作系统接口(POSIX Portable Operating System Interface)的起源。

在Linux或其他基于Unix的操作系统环境中编程相当常见。 无论您的专业知识位于何处——从物联网 (IoT) 设备和嵌入式软件开发到移动设备、超级计算或航天器——您很有可能在某个时候接触到Linux发行版。

本章内容:

  • 熟悉操作系统的概念
  • 了解Linux 内核
  • 介绍系统调用接口和系统编程
  • 浏览文件、进程和线程
  • 使用init和systemd运行服务
  • POSIX
    技术要求
    为了熟悉编程环境,读者必须准备以下内容:

1.1 熟悉操作系统的概念

现代操作系统是一个复杂的实体。 它还具有附加功能,如统计数据收集、多媒体处理、系统安全保障、整体稳定性、可靠的错误处理等。

虽然操作系统有义务执行所有这些任务,但程序员仍然有必要注意系统的细节和要求。 例如,通过虚拟机从更高的抽象层次进行工作并不意味着放弃了解我们的代码如何影响系统行为的需要。 更接近操作系统层的程序员也需要有效地管理系统资源。 这是操作系统提供应用程序编程接口(API)的原因之一。 了解如何使用此类API以及它们提供的好处是非常宝贵的专业知识。

1.1.1 操作系统类型

通用操作系统 (GPOS general-purpose operating systems) 最初是作为分时操作系统开始的。 历史上,还有另一种操作系统,与分时操作系统起源于同一时期——实时操作系统(RTOS real-time operating systems)。我们将讨论任务优先级、定时器值、外设速度、中断和信号处理程序、多线程和动态内存分配等属性如何导致系统行为的变化。 有时这些是不可预测的。 这就是为什么我们认识两种类型的RTOS:硬RTOS和软RTOS。 硬RTOS通常与给定的硬件严格相关。 系统开发人员熟悉终端设备的要求。 尽管设备的输入仍然被视为异步且不可预测,但可以初步评估和编程任务执行时间。 本书的重点仍然是具有一些软RTOS功能的GPOS编程。

在硬RTOS中,保证实时任务按时执行。系统反应期限通常是预先定义的,关键任务数据存储在ROM中,因此无法在运行时更新。 虚拟内存等功能通常被删除。 一些现代CPU内核提供所谓的紧耦合存储器 (TCM tightly coupled memory),在系统启动时将常用的数据和代码行从非易失性存储器(NVM non-volatile memory)加载到其中。 系统的行为是预先编写好的脚本。 这些操作系统的作用与机器控制有关,禁止用户输入。

软RTOS为关键任务提供最高优先级,直至完成且不会中断。 尽管如此,实时任务还是希望能够及时完成,而不应该无休止地等待。 显然,这种类型的操作系统不能用于关键任务:工厂机器、机器人、车辆等。 但它可以用来控制整个系统行为,因此这种类型的操作系统常见于多媒体和研究项目、人工智能、计算机图形、虚拟现实设备等中。 由于这些RTOS与GPOS不冲突,因此可以与其集成。 它们的功能也可以在某些Linux发行版中找到。 QNX是对此的一个有趣的实现。

1.1.2 Linux简介

Linux是一个类Unix操作系统,这意味着它提供了与Unix类似(有时是相同)的接口 - 它的功能,尤其是API,被设计为与Unix的功能相匹配。 但它不是基于Unix的操作系统。 它们的功能不是以相同的方式实现的。 对FreeBSD-macOS关系的理解也存在类似的误解。 尽管两者共享很大一部分代码,但它们的方法完全不同,包括内核的结构方式。

记住这些事实很重要,因为并非我们将在本书中使用的所有功能都存在或在所有类Unix操作系统上都可以访问。 我们专注于 Linux,只要满足每章各自的技术要求,我们的示例就可以工作。

做出这个决定有几个原因。 首先,Linux是开源的,你可以轻松查看其内核代码:https://github.com/torvalds/linux。 您应该能够轻松阅读它,因为它是用C编写的。尽管C不是面向对象的语言,但Linux内核遵循许多面向对象编程 (OOP object-oriented programming) 范例。 操作系统本身由许多独立的设计块组成,称为模块。 您可以根据您的系统需求轻松配置、集成和应用它们。 Linux给你使用实时系统(在本章后面描述)和并行代码执行的能力。 简而言之 – Linux 易于适应、扩展和配置; 我们可以轻松地利用这一点来发挥我们的优势。 但具体在哪里呢?

好吧,我们可以开发接近操作系统的应用程序,或者我们甚至可以自己生成一些模块,这些模块可以在运行时加载或卸载。 这样的例子是文件系统或设备驱动程序。 我们将在第2章深入探讨流程实体时重新讨论这个主题。 现在,假设模块看起来很像OOP设计:它们是可构造和可破坏的; 有时,根据内核的需要,可以将公共代码概括为一个模块,并且这些模块具有层次依赖性。 尽管如此,Linux内核仍被认为是整体的; 例如,它具有复杂的功能,但整个操作系统都在内核空间中运行。 相比之下,微内核(QNX、MINIX或L4)构成了运行操作系统的最低限度。 在这种情况下,附加功能是通过在内核本身之外工作的模块提供的。 这让我们对Linux内核的可能性有了一个稍微混乱但总体清晰的认识。

1.2 了解Linux内核

Linux系统中看到的三个主要层:用户空间(正在运行的进程及其线程)、内核空间(正在运行的内核本身,通常是它自己的进程)和计算机——这可以是任何类型的计算设备,例如 PC、平板电脑、 智能手机、超级计算机、物联网设备等。 正如我们在接下来的章节中解释的那样,图中观察到的所有术语都会一一落实到位,所以如果您现在不熟悉所有术语,请不要担心。

上图中的一些相互依赖关系可能已经给您留下了深刻的印象。 例如,查看设备驱动程序、各个设备和中断之间的关系。 设备驱动程序是字符设备驱动程序、块设备驱动程序和网络设备驱动程序的概括。 请注意中断与任务调度的关系。 这是一个微不足道但基本的机制,在驱动程序的实现中大量使用。 它是操作系统和硬件的初始通信和控制机制。

仅举一个例子:假设您想要从磁盘 (NVM) 恢复和读取文件,并且您通过某种标准编程功能请求它。 read() 调用将在后台执行,然后转换为文件系统操作。 文件系统调用设备驱动程序来查找并检索给定文件描述符后面的内容,然后将其与文件系统已知的地址相关联。 所需设备 (NVM) 开始搜索数据片段——文件。 在操作完成之前,如果调用者进程是单线程进程并且没有其他事情可做,则它将被停止。 另一个进程将开始工作,直到设备找到并返回指向文件地址的指针。 然后触发中断,这有助于操作系统调用调度程序。 我们的初始进程将使用新加载的数据再次启动,第二个进程现在将停止。

此任务示例演示了如何通过一个小的、无关紧要的操作来影响系统的行为 - 这是您将在第一个编程课程中学习的编码。 在系统的生命周期中,许多进程将一直重新安排。 操作系统的工作就是在不中断的情况下实现这一点。

但中断是一项繁重的操作,可能会导致不必要的内存访问和无用的应用程序状态切换。现在,想想如果系统过载会发生什么——CPU使用率达到99%,或者磁盘收到很多请求而无法及时处理它们。 如果该系统是飞机嵌入式设备的一部分怎么办? 当然,这在现实中不太可能,因为飞机需要满足严格的技术要求和高质量标准。 但为了便于讨论,请考虑如何防止类似情况发生,或者如何保证代码在任何用户场景中成功执行。

1.3 系统调用接口和系统编程

NVM数据请求是一个受益于系统调用接口的过程,因为操作系统有义务将此请求转换为应用程序二进制接口 (ABI application binary interface) 调用,引用相应的设备驱动程序。 这种操作称为系统调用。使用系统调用来实现或执行操作系统提供的功能称为系统编程。 系统调用是内核服务的唯一入口点。 它们通常由glibc等库包装,并且不直接调用。

换句话说,系统调用定义了程序员的接口,通过该接口可以使用所有内核服务。 操作系统更多地可以被视为内核服务和硬件之间的中介。 除非你喜欢摆弄硬件引脚和底层平台指令,或者你自己就是模块架构师,否则你应该勇敢地将细节留给操作系统。操作系统负责处理特定的计算机物理接口操作。应用程序有责任使用正确的系统调用。软件工程师的任务是了解它们对系统整体行为的影响。 请记住,使用系统调用是有代价的。

正如示例中所观察到的,操作系统在检索文件时会执行很多操作。当动态分配内存或由多个线程访问单个内存块时,甚至会完成更多工作。我们将在接下来的章节中进一步讨论这一点,并将强调尽可能谨慎地使用系统调用,无论是自愿还是非自愿。 简而言之,系统调用不是简单的函数调用,因为它们不是在用户空间中执行的。 系统调用不会进入程序堆栈中的下一个过程,而是触发模式切换,从而导致跳转到内核内存堆栈中的例程。 从文件中读取可以可视化如下:

那么我们什么时候应该使用系统调用呢? 简而言之,当我们想要非常精确地执行某些操作系统任务时,这些任务通常与设备管理、文件管理、进程控制或通信基础设施相关。 我们将在后面的章节中介绍这些角色的许多示例,但简而言之,欢迎您阅读更多内容并熟悉以下内容:

  • syscall()
  • fork()
  • exec()
  • exit()
  • wait()
  • kill())

参考:https://www.kernel.org/doc/man-pages/。

有用系统调用的简短列表可以在以下链接中找到:https://man7.org/linux/man-pages/man2/syscalls.2.xhtml。

您可能已经猜到,使用系统调用接口也会给系统带来安全风险。 距离内核和设备控制如此之近,为恶意软件渗透您的软件提供了绝佳的机会。 当您的软件影响系统行为时,另一个程序可能会嗅探并收集有价值的数据。 您至少可以做的就是设计代码,使用户界面与关键过程(尤其是系统调用)很好地隔离。 100%安全是不可能的,虽然有许多关于安全问题的综合书籍,但保护系统安全的艺术本身就是一个不断发展的过程。

1.4 浏览文件、进程和线程

1.4.1 文件

简而言之,我们需要文件来代表系统上的多种资源。 我们编写的程序也是文件。 编译后的代码,例如可执行二进制文件(.bin、.exe)和库都是文件(.o、.so、.lib、.dll 等)。 此外,我们还需要它们来实现通信机制和存储管理。 你知道Linux上可以识别哪些类型的文件吗? 让我们快速向您介绍一下:

  • 普通或常规文件:系统上几乎所有存储数据的文件都被视为常规文件:文本、媒体、代码等。
  • 目录:用于构建文件系统的层次结构。 它们不存储数据,而是存储其他文件的位置。
  • 特殊(设备)文件:您可以在 /dev 目录下找到它们,代表您的所有硬件设备。
  • 链接:我们使用它们来允许访问不同位置的另一个文件。 实际上,它们是真实文件的替换,通过它们可以直接访问这些文件。 这与 Windows的快捷方式不同。 它们是特定的文件类型,需要应用程序来支持它们 - 首先处理快捷方式元数据,然后指向资源,因此文件不会被一次性访问。
  • 套接字:这是进程交换数据(包括与其他系统)的通信端点。
  • 命名管道:我们使用命名管道在系统上当前运行的两个进程之间交换双向数据。

1.4.2 进程和线程

进程是程序的一个实例,准确地说是一个执行实例。 它有自己的地址空间并与其他进程保持隔离。 这意味着每个进程都有操作系统分配给它的一系列(通常是虚拟的)地址。 Linux将它们视为任务。 一般用户无法观察到它们。 这就是内核完成其工作的方式。 每个任务都通过在include/linux/sched.h中定义的task_struct实体进行描述。 系统管理员和系统程序员通过进程表观察进程,进程表通过每个进程的特定进程标识符(pid )进行散列。 此方法用于快速查找进程-使用终端中的 ps 命令查看系统上的进程状态,然后键入以下命令查看单个进程的具体信息:

ps -p <required pid>

新进程是通过当前进程属性的副本创建的,并且属于进程组。 一个或多个组创建一个会话。 每个会话都与一个终端相关。 小组和会议都有流程领导者。 属性的克隆主要用于资源共享。 如果两个进程共享相同的虚拟内存空间,它们将被视为单个进程中的两个线程并进行管理,但它们不像进程量级。

我们关心四个实体:第一个是可执行文件,因为它是要执行的指令的单元载体。 其次是进程——执行这些指令的工作单元。 第三,我们需要这些指令作为处理和管理系统资源的工具。 第四是线程——最小的指令序列,由操作系统独立管理,是进程的一部分。请记住,每个操作系统的进程和线程的实现都不同,因此在使用它们之前请先进行研究。

从内核的角度来看,进程的主线程是任务组组长,在代码中标识为group_leader。 由组领导者产生的所有线程都可以通过 thread_node 进行迭代。实际上,它们存储在一个单链表中,thread_node是它的头。生成的线程携带一个指向 group_leader 工具的指针。 进程创建者的task_struct对象由它指向。 你可能已经猜对了,它与组长的task_struct相同。

如果一个进程生成另一个进程,例如通过 fork(),新创建的进程(称为子进程)通过父指针了解其创建者。 它们还通过兄弟指针了解其兄弟进程,该指针是指向父进程的其他子进程的列表节点。 每个父级都通过子级了解其子级——指向列表头的指针,存储子级并提供对它们的访问。

如下图所示,线程没有定义任何其他数据结构:

我们已经多次提到 fork(),但它到底是什么? 嗯,简单来说,它是一个系统函数,用于创建进程调用者的进程副本。 它向父进程提供新进程的 ID 并启动子进程的执行。

在幕后,fork()被替换为clone()。 通过标志提供不同的选项,但如果全部设置为零,clone()的行为类似于 fork()。更多参考:https://man7.org/linux/man-pages/man2/clone.2.xhtml。

您可能会问自己为什么这种实现更可取。 这样想:当内核在进程之间进行切换时,它会检查当前进程在虚拟内存中的地址,确切地说是页目录。 如果与新执行的进程相同,则它们共享相同的地址空间。 那么,switch只是一个简单的指针跳转指令,通常是到程序的入口点。 这意味着预计重新安排会更快。 请小心–进程可能共享相同的地址空间,但不共享相同的程序堆栈。 clone()负责为每个进程创建不同的堆栈。

1.4.3 基于运行模式的进程类型

某些流程需要启动或交互用户交互。它们被称为前台进程。 但正如您可能已经发现的那样,有些进程独立于我们或任何其他用户的活动运行。此类进程称为后台进程。除非另有说明,否则作为程序执行调用或用户命令的终端输入默认被视为前台进程。要在后台运行进程,只需将&放在用于启动该进程的命令行末尾即可。 例如,让我们调用已知的测试,完成后,我们在终端中看到以下内容:

$ ./test &
[1] 62934
[1]  + done       ./test
$ ./test &
[1] 63388
$ kill 63388
[1]  + terminated./test

终止进程和让它自行终止是两件不同的事情,终止进程可能会导致不可预测的系统行为或无法访问某些资源,例如未关闭的文件或套接字。

一些进程在无人值守的情况下运行。它们被称为守护进程并在后台持续运行。他们预计将始终可用。守护进程通常通过系统的启动脚本启动并运行直到系统关闭。它们通常提供系统服务,并且多个用户依赖它们。 因此,启动时的守护进程通常由ID为 0的用户(通常是 root)启动,并且可能以root权限运行。

Linux系统上拥有最高权限的用户称为root用户,或简称为root。 此权限级别允许执行与安全相关的任务。该角色对系统的完整性有直接影响,因此所有其他用户必须设置尽可能最小的权限级别,直到需要更高的权限级别。

僵尸进程是已终止但仍可通过其pid识别的进程。 它没有地址空间。 只要其父进程运行,僵尸进程就会继续存在。 这意味着,在我们退出主进程、关闭系统或重新启动之前,僵尸进程在ps列出时仍将显示为

$ ps
  PID TTY           TIME CMD
…
64690 ttys000    0:00.00 <defunct>
$ top
t–p - 07:58:26 up 100 days,  2:34, 2 users,  load average: 1.20, 1.12, 1.68
Tasks: 200 total,   1 running, 197 sleeping,   1 stopped,   1 zombie

1.5 用init和systemd 运行服务

是初始进程,在Linux系统上由内核执行,其pid始终为1:

$ ps -p 1
PID TTY          TIME CMD
1 ?        04:53:20 systemd

它被称为系统上所有进程的父进程,因为它用于初始化、管理和跟踪其他服务和守护程序。 Linux的第一个init守护进程称为Init,它定义了六种系统状态。 所有系统服务都分别映射到这些状态。 它的脚本用于按预定义的顺序启动进程,系统程序员偶尔会使用它。 使用它的一个可能的原因是减少系统的启动持续时间。 要创建服务或编辑脚本,您可以修改/etc/init.d,ls命令并查看可以通过init运行的所有服务。

$ ls /etc/init.d/
acpid
alsa-utils
anacron
...
ufw
unidd
x11-common

每个脚本都遵循相同的代码模板来执行和维护:

您可以自己生成相同的模板,并通过以下命令阅读有关init脚本源代码的更多信息:“$ man init-d-script”

您可以通过以下命令列出可用服务的状态:

$ service --status-all
[ + ] acpid
[ - ] alsa-utils
[ - ] anacron
...
[ + ] ufw
[ - ] uuidd
[ - ] x11-common

我们可以停止防火墙服务 – ufw:

$ service ufw stop

现在,让我们检查一下它的状态:

$ service ufw status
● ufw.service - Uncomplicated firewall
...
$ service ufw start
$ service ufw status
● ufw.service - Uncomplicated firewall
...

以类似的方式,您可以创建自己的服务并使用service 命令启动它。 在现代、全面的Linux系统上,init被认为是一种过时的方法。 尽管如此与systemd不同,它可以在每个基于Unix的操作系统上找到,因此系统程序员会期望将其用作服务的通用接口。 因此,我们更多地使用它作为一个简单的示例和服务来自哪里的解释。 如果我们想使用最新的方法,我们必须转向 systemd。

systemd是一个init守护进程,它代表了在Linu 系统上运行服务的现代方法。 它提供了并行系统服务启动功能,这进一步加快了初始化过程。每个服务都存储在/lib/systemd/system或/etc/systemd/system目录下的.service 文件中。 /lib 中的服务是系统启动服务的定义,/etc中的服务是系统运行时启动的服务的定义。 让我们列出它们:

$ ls /lib/systemd/system
accounts-daemon.service
acpid.path
acpid.service
...
$ ls /etc/systemd/system
bluetooth.target.wants
display-manager.service
…
timers.target.wants
vmtoolsd.service

systemd的接口比init复杂得多。 我们鼓励您花时间单独检查它,因为我们无法在这里对其进行简短总结。 但是如果您列出systemd目录,您可能会观察到许多类型的文件。 在守护进程的上下文中,它们被称为单元。它们每个都提供不同的接口,因为它们每个都与systemd管理的某个实体相关。 每个文件内的脚本描述了设置的选项以及给定服务的作用。 单位名称响亮。.timer 用于计时器管理,.service 用于指定给定服务的启动方式及其依赖项,.path 描述给定服务的基于路径的激活,等等。

让我们创建一个简单的systemd 服务,其目的是监视给定文件是否被修改。
首先,让我们通过一个简单的文本编辑器创建一些虚拟文件。 让我们想象这是一个真实的配置。 打印出来给出以下内容:

$ cat /etc/test_config/config
test test

让我们准备一个脚本来描述文件更改时需要执行的过程。 同样,为了本示例的目的,让我们通过一个简单的文本编辑器创建它 - 它将如下所示:

$ cat ~/sniff_printer.sh
echo "File /etc/test_config/config changed!"

当调用脚本时,会出现一条消息,表明文件已更改。 当然,您可以将任何程序放在这里。 我们将其称为sniff_printer,因为我们正在通过该服务嗅探文件更改,并且我们将打印一些数据。

那么这是怎么发生的呢? 首先,我们通过unit – myservice_test.service定义新服务,并实现以下脚本:

# /etc/systemd/system/myservice_test.service
[Unit]
Description=This service is triggered through a file change
[Service]
Type=oneshot
ExecStart=bash /home/oem/sniff_printer.sh
[Install]
WantedBy=multi-user.target

其次,我们通过另一个名为myservice_test.path的单元描述我们正在监视的文件路径,该单元通过以下代码实现:

# /etc/systemd/system/myservice_test.path
[Unit]
Description=Path unit for watching for changes in "config"
[Path]
PathModified=/etc/test_config/config
Unit=myservice_test.service
[Install]
WantedBy=multi-user.target

将所有这些部分组合在一起,我们得到一个可以打印出简单消息的服务。 每当提供的文件更新时都会触发它。 让我们看看进展如何。 当我们向服务目录添加新文件时,我们必须执行重新加载:

$ systemctl daemon-reload
$ systemctl enable myservice_test

Created symlink /etc/systemd/system/multi-user.target.wants/myservice_test.service → /etc/systemd/system/myservice_test.service.
(base) andrew@andrew-YTF-XXX:~/code$ sudo systemctl start myservice_test
(base) andrew@andrew-YTF-XXX:~/code$ systemctl status myservice_test
○ myservice_test.service - This service is triggered through a file change
     Loaded: loaded (/etc/systemd/system/myservice_test.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Wed 2023-12-06 17:26:22 CST; 10s ago
    Process: 1854871 ExecStart=bash /home/andrew/sniff_printer.sh (code=exited, status=0/SUCCESS)
   Main PID: 1854871 (code=exited, status=0/SUCCESS)
        CPU: 5ms

12月 06 17:26:22 andrew-YTF-XXX systemd[1]: Starting This service is triggered through a file change...
12月 06 17:26:22 andrew-YTF-XXX bash[1854871]: File /etc/test_config/config changed!
12月 06 17:26:22 andrew-YTF-XXX systemd[1]: myservice_test.service: Deactivated successfully.
12月 06 17:26:22 andrew-YTF-XXX systemd[1]: Finished This service is triggered through a file change.

$ systemctl start myservice_test
$ systemctl status myservice_test
○ myservice_test.service - This service is triggered through a file change
     Loaded: loaded (/etc/systemd/system/myservice_test.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Wed 2023-12-06 17:47:54 CST; 16s ago
    Process: 1855368 ExecStart=bash /home/andrew/sniff_printer.sh (code=exited, status=0/SUCCESS)
   Main PID: 1855368 (code=exited, status=0/SUCCESS)
        CPU: 4ms

12月 06 17:47:54 andrew-YTF-XXX systemd[1]: Starting This service is triggered through a file change...
12月 06 17:47:54 andrew-YTF-XXX bash[1855368]: File /etc/test_config/config changed!
12月 06 17:47:54 andrew-YTF-XXX systemd[1]: myservice_test.service: Deactivated successfully.
12月 06 17:47:54 andrew-YTF-XXX systemd[1]: Finished This service is triggered through a file change.

我们需要通过一些文本编辑器更新该文件,例如:

$ vim /etc/test_config/config
$ systemctl status myservice_test
● myservice_test.service
...

但该进程不再处于活动状态,因为服务单元的类型为oneshot,因此只有另一个文件更新才会重新触发它。 我们相信这个示例提供了如何在系统运行时创建和启动守护进程的简单解释。 请随意尝试自己并尝试不同的单位类型或选项。

参考资料

1.6 POSIX

POSIX标准的主要任务是维护不同操作系统之间的兼容性。 因此,POSIX 被标准应用软件开发人员和系统程序员频繁使用。 如今,它不仅可以在类Unix操作系统上找到,还可以在Windows环境中找到,例如Cygwin、MinGW和 Windows Subsystem for Linux (WSL)。 POSIX定义了系统级和用户级API,但有一点是:使用POSIX,程序员不需要区分系统调用和库函数。

POSIX API经常在C编程语言中使用。 因此它可以与C++编译。 在系统编程的几个重要领域中,为系统调用接口提供了附加功能:文件操作、内存管理、进程和线程控制、网络和通信以及正则表达式 - 正如您所看到的,它几乎涵盖了已经提供的所有内容。 现有的系统调用可以。 只是不要感到困惑并认为情况总是如此。

与每个标准一样,POSIX有多个版本,您必须了解系统中存在哪一个版本。 它还可以是某些环境子系统的一部分,例如Windows的Microsoft POSIX 子系统。 这是一个关键的评论,因为环境本身可能不会向您公开整个界面。 原因之一可能是系统的安全评估。

随着POSIX的发展,代码质量的规则已经建立。 其中一些与多线程内存访问、同步机制和并发执行、安全和访问限制以及类型安全有关。 POSIX软件需求中的一个著名概念是“一次编写,随处采用”。

标准定义和目标它的应用有四个主要领域,称为卷:

  • 基本定义:规范的主要定义:语法、概念、术语和服务操作
  • 系统接口:接口描述和定义的可用性
  • 实用程序:Shell、命令和实用程序描述
  • 理由:版本信息和历史数据

尽管如此,在本书中我们的重点主要是POSIX作为系统调用的一种不同方法。 在接下来的章节中,我们将看到使用消息队列、信号量、共享内存或线程等对象的通用模式的好处。 一个显着的改进是函数调用及其命名约定的简单性。 例如shm_open()、mq_open()和sem_open() 分别用于创建和打开共享内存对象、消息队列和信号量。 他们的相似之处是显而易见的。POSIX 中类似的想法受到系统程序员的欢迎。API 也是公开的,并且有大量的社区贡献。 此外POSIX还提供了一个对象接口,例如互斥体,这在Unix上并不常见。 然而,在后面的章节中,我们将建议读者更多地关注C++20功能,这是有充分理由的,所以请耐心等待。

使用POSIX允许软件工程师概括他们的操作系统相关代码并将其声明为不特定于操作系统。 这样可以更轻松、更快速地重新集成软件,从而缩短上市时间。 系统程序员还可以轻松地从一个系统切换到另一个系统,同时仍然编写相同类型的代码。

1.7 概括

在本章中,我们介绍了与操作系统相关的基本概念的定义。 您已经了解了Linux的主要内核结构及其对软件设计的期望。 简要介绍了实时操作系统,还介绍了系统调用的定义、系统调用接口和POSIX。 我们还奠定了多处理和多线程的基础。 在下一章中,我们将讨论作为主要资源用户和管理者的进程。 我们将从一些C++20代码开始。 通过本文,您将了解Linux的进程内存布局、操作系统的进程调度机制,以及多处理如何运行以及它带来的挑战。 您还将了解一些有关原子操作的有趣事实。